<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Octopus blog</title>
  <subtitle>Site description.</subtitle>
  <link href="https://octopus.com/blog/feed.xml" rel="self" />
  <link href="https://octopus.com" />
  <id>https://octopus.com/blog/feed.xml</id>
  <updated>2026-10-13</updated>

    <entry>
      <title>Introducing remote MCP for Octopus Deploy</title>
      <link href="https://octopus.com/blog/introducing-remote-mcp" />
      <id>https://octopus.com/blog/introducing-remote-mcp</id>
      <published>2026-10-13</published>
      <updated>2026-10-13</updated>
      <summary>Octopus now includes a built-in remote MCP server. There's nothing to install or maintain: point your agent at your instance's /mcp endpoint.</summary>
      <author>
        <name>Gin Atkins, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Until now, connecting an agent to Octopus meant running our local Model Context Protocol (MCP) server on your machine. You had to install it on every machine, for every user, and keep every installation up to date.</p>
<p>There’s an easier option now. Octopus includes a built-in remote MCP server, so there’s nothing to install or maintain. Point your agent at your instance’s <code>/mcp</code> endpoint, and it can talk to Octopus over HTTP.</p>
<h2 id="what-can-the-remote-mcp-do">What can the remote MCP do?</h2>
<p>The remote MCP initially supports a selected set of core Octopus features, and we’ll add more over time. <a href="https://srfjzffylk.zite.so">Share your feedback</a> to help us decide what comes next.</p>
<p>Depending on the authentication mechanism and the permissions you grant, the remote MCP can:</p>
<ul>
<li>
<p><strong>Navigate and orient:</strong> Find and provide details on spaces, projects, tenants, and tenant tags.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">List the projects in the Payments space and the tenants each one deploys to.</span></span></code></pre>
</li>
<li>
<p><strong>Answer deployment questions:</strong> Show a pull request’s deployment status, including when it reached a tenant or environment, and describe the lifecycle phases and progression rules behind it.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">When did PR #4821 reach the Brisbane Bank tenant, and which environment is it in now?</span></span></code></pre>
</li>
<li>
<p><strong>Deploy and operate:</strong> Create and deploy a release, promote a release through its lifecycle, and run runbooks. Then generate and attach release notes to a release.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a release of Payments Service, deploy it to production, then draft and attach release notes.</span></span></code></pre>
</li>
<li>
<p><strong>Investigate and diagnose:</strong> Search task logs, deployment processes, and variables to work out why something failed. List or filter running, queued, and failed tasks across the instance.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">The last deployment of Payments Service to production failed. What was the error, and which step failed?</span></span></code></pre>
</li>
<li>
<p><strong>Audit and report:</strong> Query audit events, report on deployment health, and triage incidents by tracing what was deployed to which targets and when.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">What changed in the Payments space in the last hour?</span></span></code></pre>
</li>
<li>
<p><strong>Check tenant configuration:</strong> Find variables with incomplete configuration across tenants.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Which tenants have missing variable values for the Payments project?</span></span></code></pre>
</li>
</ul>
<h2 id="get-started">Get started</h2>
<p>Octopus Cloud customers can use the remote MCP now. Point your agent at your instance’s <code>/mcp</code> endpoint to try it.</p>
<p>Octopus Server customers will get it in the upcoming 2026.3 release.</p>
<p>Read the <a href="https://octopus.com/docs/octopus-ai/mcp/remote">remote MCP docs</a> to learn how to connect your agent and set up authentication.</p>
<h2 id="whats-next">What’s next</h2>
<p>We know that for many teams, easily connecting an agent to Octopus is only the first step. The next is governance.</p>
<p>The remote MCP is one part of our broader vision for agent governance. In the coming months, we’re launching:</p>
<ul>
<li><strong>Scoped API keys:</strong> Tighter control over what an agent can reach</li>
<li><strong>OAuth for the remote MCP:</strong> A cleaner, more secure connection flow</li>
<li><strong>Agent-aware policies:</strong> Visibility into whether an agent initiated a deployment or runbook run, to inform Platform Hub policies</li>
<li><strong>Improved audit log:</strong> Easier filtering and viewing of audit events related to agent activity</li>
</ul>
<p>Thank you to the 19 users who voted for and commented on remote MCP in our <a href="https://octopusdeploy.productboard.com/detail/MTpQbUVudGl0eTpjZWM2MWFiOS0yYWEzLTQwZTgtODhhOC1mOTczNjUwMWVjNWE=/portal">roadmap</a>. You helped shape this feature.</p>]]></content>
    </entry>
    <entry>
      <title>Salesforce Deployments with Octopus</title>
      <link href="https://octopus.com/blog/salesforce-deployments" />
      <id>https://octopus.com/blog/salesforce-deployments</id>
      <published>2026-10-10</published>
      <updated>2026-10-10</updated>
      <summary>Learn how to deploy Salesforce applications using Octopus Deploy</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p><a href="https://developer.salesforce.com/developer-centers/developer-experience">Salesforce DX</a> (Developer Experience) is a set of tools and features that bring modern, source-driven development and DevOps practices to the Salesforce platform. It allows teams to build Salesforce applications using Git-based workflows and deploy them via CI/CD pipelines.</p>
<p>Salesforce DX provides a command-line interface (CLI) to automate application deployments, and Octopus Deploy can integrate with it to deliver a seamless deployment experience.</p>
<p>In this post, you’ll learn how to deploy Salesforce applications using Octopus Deploy and the Salesforce CLI.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Download the sample application from <a href="https://github.com/OctopusSolutionsEngineering/SalesforceSampleApp">this GitHub repository</a>. The <a href="https://github.com/OctopusSolutionsEngineering/SalesforceSampleApp/releases">releases</a> provide Zip files that can be uploaded to the Octopus built-in feed:</p>
<p><img src="/blog/i/x/salesforce-deployments/built-in-feed.png" alt="Built-in feed" loading="lazy" srcset="/blog/i/600/salesforce-deployments/built-in-feed.webp 600w, /blog/i/1000/salesforce-deployments/built-in-feed.webp 1000w, /blog/i/2000/salesforce-deployments/built-in-feed.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3758" height="1930"></p>
<p>You must install the Salesforce CLI locally by following the <a href="https://developer.salesforce.com/tools/salesforcecli">Salesforce CLI installation instructions</a>.</p>
<p>You can create a free account on the <a href="https://developer.salesforce.com/">Salesforce Developer site</a>. This post will use a Salesforce Developer Edition account to deploy the sample application.</p>
<h2 id="log-in-locally">Log in locally</h2>
<p>Log in to your Salesforce Developer Edition account using the Salesforce CLI:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> org</span><span style="color:#A31515"> login</span><span style="color:#A31515"> web</span><span style="color:#0000FF"> --set-default</span><span style="color:#0000FF"> --alias</span><span style="color:#A31515"> Dev</span></span></code></pre>
<p>The authentication URL, which is used as a secret to log in to Salesforce, is shown with this command:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> org</span><span style="color:#A31515"> auth</span><span style="color:#A31515"> show-sfdx-auth-url</span></span></code></pre>
<p>The <code>SFDX Auth URL</code> is then displayed in the format <code>force://PlatformCLI::&#x3C;secret>@&#x3C;hostname>.develop.my.salesforce.com</code>. This URL is used to authenticate the Salesforce CLI with your Salesforce organization. Take note of this URL, as it must be saved as a secret in Octopus Deploy.</p>
<div class="hint"><p>You can use the <a href="https://developer.salesforce.com/docs/platform/sfdx-dev/guide/sfdx-dev-auth-jwt-flow.html">JWT Flow</a> to authenticate the Salesforce CLI without requiring a web-based login. This allows deployments to be automated without user interaction. However, this post will use the web-based login for simplicity.</p></div>
<h2 id="deployment-process">Deployment process</h2>
<p>Deploying a Salesforce application is done via a script step that runs the Salesforce CLI commands.</p>
<p>To automate Salesforce login, the <code>SFDX Auth URL</code> is stored as a secret variable named <code>Salesforce.Auth</code>.</p>
<p>Then add a script step to the deployment process that runs the following commands.</p>
<p>You’ll make use of an Execution Container to expose the Salesforce CLI to the script step. In the <code>Execution Container Image</code> section, select the option <code>Runs inside a container on a worker</code>, and then the <code>Build container from an inline Dockerfile</code> option. Paste the following Dockerfile into the text box:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="dockerfile"><code><span class="line"><span style="color:#0000FF">FROM</span><span style="color:#000000"> node:22-bookworm-slim</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0000FF">RUN</span><span style="color:#000000"> apt-get update &#x26;&#x26; apt-get install -y --no-install-recommends \</span></span>
<span class="line"><span style="color:#000000">    ca-certificates \</span></span>
<span class="line"><span style="color:#000000">    curl \</span></span>
<span class="line"><span style="color:#000000">    gnupg \</span></span>
<span class="line"><span style="color:#000000">    libicu-dev \</span></span>
<span class="line"><span style="color:#000000">    &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0000FF">RUN</span><span style="color:#000000"> npm install -g @salesforce/cli</span></span></code></pre>
<p>Paste the following script as an inline script in the script step:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#008000"># Stop the CLI from losing its auth storage context</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SF_USE_GENERIC_UNIX_KEYCHAIN</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Deactivate the CLI's DNS check, which frequently fails in CI networks</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SF_DISABLE_DNS_CHECK</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SFDX_DISABLE_DNS_CHECK</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Keep telemetry off</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SF_DISABLE_TELEMETRY</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Enter the directory with the sample application</span></span>
<span class="line"><span style="color:#795E26">cd</span><span style="color:#A31515"> SampleApp</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Log into Salesforce using the auth URL stored in Octopus Deploy</span></span>
<span class="line"><span style="color:#795E26">echo</span><span style="color:#000000"> $(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "Salesforce.Auth"</span><span style="color:#000000">) > </span><span style="color:#A31515">./authfile.txt</span></span>
<span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> org</span><span style="color:#A31515"> login</span><span style="color:#A31515"> sfdx-url</span><span style="color:#0000FF"> --sfdx-url-file</span><span style="color:#A31515"> ./authfile.txt</span><span style="color:#0000FF"> --set-default</span><span style="color:#0000FF"> --alias</span><span style="color:#A31515"> CiOrg</span></span>
<span class="line"><span style="color:#795E26">rm</span><span style="color:#A31515"> ./authfile.txt</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Perform the deployment</span></span>
<span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> project</span><span style="color:#A31515"> deploy</span><span style="color:#A31515"> start</span><span style="color:#0000FF"> --target-org</span><span style="color:#A31515"> CiOrg</span></span></code></pre>
<p>Finally, add the sample application to the project as a referenced package:</p>
<p><img src="/blog/i/x/salesforce-deployments/referenced-package.png" alt="Referenced package" loading="lazy" srcset="/blog/i/600/salesforce-deployments/referenced-package.webp 600w, /blog/i/1000/salesforce-deployments/referenced-package.webp 1000w, /blog/i/2000/salesforce-deployments/referenced-package.webp 1434w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1434" height="1492"></p>
<h2 id="using-the-new-component">Using the new component</h2>
<p>To use the new component from the sample application, navigate to the <code>Setup</code> menu in Salesforce:</p>
<p><img src="/blog/i/x/salesforce-deployments/setup-menu.png" alt="Setup" loading="lazy" srcset="/blog/i/600/salesforce-deployments/setup-menu.webp 600w, /blog/i/1000/salesforce-deployments/setup-menu.webp 740w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="740" height="900"></p>
<p>Open the Lightning App Builder item in the left-hand menu, and click the <code>New</code> button to create a new Lightning app:</p>
<p><img src="/blog/i/x/salesforce-deployments/lightning-app-builder.png" alt="New Lightning App" loading="lazy" srcset="/blog/i/600/salesforce-deployments/lightning-app-builder.webp 600w, /blog/i/1000/salesforce-deployments/lightning-app-builder.webp 1000w, /blog/i/2000/salesforce-deployments/lightning-app-builder.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></p>
<p>Complete the wizard to create a new Lightning app:</p>
<p><img src="/blog/i/x/salesforce-deployments/new-lightning-page-1.png" alt="New Lightning App Wizard Page 1" loading="lazy" srcset="/blog/i/600/salesforce-deployments/new-lightning-page-1.webp 600w, /blog/i/1000/salesforce-deployments/new-lightning-page-1.webp 1000w, /blog/i/2000/salesforce-deployments/new-lightning-page-1.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2008" height="1218"></p>
<p><img src="/blog/i/x/salesforce-deployments/new-lightning-page-2.png" alt="New Lightning App Wizard Page 2" loading="lazy" srcset="/blog/i/600/salesforce-deployments/new-lightning-page-2.webp 600w, /blog/i/1000/salesforce-deployments/new-lightning-page-2.webp 1000w, /blog/i/2000/salesforce-deployments/new-lightning-page-2.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2036" height="1214"></p>
<p><img src="/blog/i/x/salesforce-deployments/new-lightning-page-3.png" alt="New Lightning App Wizard Page 3" loading="lazy" srcset="/blog/i/600/salesforce-deployments/new-lightning-page-3.webp 600w, /blog/i/1000/salesforce-deployments/new-lightning-page-3.webp 1000w, /blog/i/2000/salesforce-deployments/new-lightning-page-3.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2014" height="1194"></p>
<p>Select the new component and drag it onto the page:</p>
<p><img src="/blog/i/x/salesforce-deployments/sample-app.png" alt="Gui Builder" loading="lazy" srcset="/blog/i/600/salesforce-deployments/sample-app.webp 600w, /blog/i/1000/salesforce-deployments/sample-app.webp 1000w, /blog/i/2000/salesforce-deployments/sample-app.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></p>
<h2 id="going-further">Going further</h2>
<p>This <a href="https://developer.salesforce.com/blogs/2020/01/using-salesforce-dx-with-github-actions">Salesforce post</a> describes a high-level deployment pipeline, with this diagram showing the breakdown between the CI and CD processes:</p>
<p><img src="/blog/i/x/salesforce-deployments/example-flow.png" alt="Example Flow" loading="lazy" srcset="/blog/i/600/salesforce-deployments/example-flow.webp 442w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="442" height="742"></p>
<p>All the steps in the CD process are implemented via the <code>sf</code> CLI and can be automated using Octopus Deploy.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Octopus provides a convenient way to deploy Salesforce applications using the Salesforce CLI. The CLI handles the interactions with the Salesforce platform, while Octopus provides a user-friendly interface for managing deployments, variables, and secrets. By combining these tools, teams can implement a robust CI/CD pipeline for Salesforce applications.</p>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.10: Modernizing legacy software</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e10" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e10</id>
      <published>2026-10-06</published>
      <updated>2026-10-06</updated>
      <summary>Find out why big-bang rewrites are the path to failure and what to do instead.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In <a href="https://octopus.com/blog/continuous-delivery-office-e9">CD Office Hours episode 9</a>, we discussed the compliance ratchet and how accelerating software development without the right practices can lead to an irreversible increase in controls. In this episode, we look at a classic software problem: the lure of the software rewrite and the inevitable hazards such endeavors bring.</p>
<p>When the cost to maintain the software and add new features becomes burdensome, a decision is often made to rewrite it rather than improve it “in place”. This is sometimes a valid approach, but more often it’s based on an exaggeration of how bad the code is and an understatement of the work involved.</p>
<p>In some cases, the qualities of the codebase that a team dislikes stem from an architectural decision they don’t understand, either because there’s no record of it or because the records aren’t put to use. In many cases, the layers of change applied to the code over time have made it messy and hard to comprehend.</p>
<p>When considering a big-bang rewrite, it is practically impossible to determine whether the rewrite is required or will provide a return on investment. That’s why big-bang rewrites have such a high failure rate and should be avoided.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p><a href="https://www.youtube.com/watch?v=1CgHK2I7k1Y">Watch Continuous Delivery Office Hours Ep.10</a></p>
<h2 id="legacy-or-heritage">Legacy or heritage</h2>
<p>The term “legacy” has become associated with old and unusable software, but more often, legacy software is the long-running backbone of an organization. It captures vast institutional knowledge and endures for so long because it works. In many cases, software inevitably becomes legacy software, as the alternative is that it fails.</p>
<p>That’s why we prefer to use the term “heritage” software, as it’s an artifact of special value to the organization. The code for heritage software has captured not just the essence of the solution, but hundreds or even thousands of edge cases and enhancements based on years of learning.</p>
<h2 id="the-hazards-of-the-rewrite">The hazards of the rewrite</h2>
<p>Most codebases have parts that are untidy or don’t make sense. There’s a tipping point that makes us want to rewrite the whole thing, but this is often the wrong reaction. It’s very rare for the cost of maintenance to exceed the cost of wholesale replacement.</p>
<p>When teams consider a big-bang rewrite, they tend to estimate the task based on the obvious use cases. That means the new software is naturally cleaner, because it doesn’t handle any of the less obvious problems. When the new software version goes live, the team begins rediscovering the many edge cases that must be handled.</p>
<p>By the time these edge cases have been incorporated into the software, it has started to look complicated once more, because the complexity came from the domain rather than a careless implementation. The team may find more graceful ways to handle edge cases, but by this time, the rewrite will have vastly exceeded all estimates of effort and expense.</p>
<p>When the rewrite is complete, the organization will effectively be “back to square one”, which means they will have fallen behind the competition. That means the anticipated cost of maintenance and new features must be substantially lower to allow the software to catch up and pass competitor offerings.</p>
<p>Essentially, wholesale rewrites should be avoided; a different approach is needed.</p>
<h2 id="architectural-and-team-strategies">Architectural and team strategies</h2>
<p>The alternative to the big-bang rewrite is to identify a specific component to replace. You might take a calculation routine and move it out of a monolith into a service, creating the missing tests and refining the routines as you extract it. The result is a new, highly maintainable component and a slightly smaller monolith that calls it rather than doing the work itself.</p>
<p>This is the strangler fig pattern, named for the plants that cheat the competition for sunlight by growing high up on a host tree, eventually sending down their own roots and often remaining after the host tree has died and rotted away. In the same way, the heritage software can continue to support users while an increasing number of components are rewritten and deleted from the monolith.</p>
<p>You can use both domain-driven design and team topologies to inform your gradual rewrite. If you can identify a seam that would carve off a meaningful part of the domain and have a team that can own the domain and the component that serves it, you can create a meaningful interface for the software and improve the organization’s communication structure.</p>
<p>Combining the strangler fig pattern with architectural and team design results in teams with deep knowledge of their specific areas and the ability to move with relative independence. The number of teams you have can be instructive when considering how far you want to break down a monolith into cohesive components.</p>
<p>Wholesale rewrites invite disaster, but working in small steps and considering organizational design as well as architectural design is the path to success.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Continuous Delivery Office Hours is a series of conversations about software delivery, with Tony Kelly, Bob Walker, and Steve Fenton.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw3CrxkUOAMmiy928lr5D4oh">YouTube</a>, <a href="https://podcasts.apple.com/us/podcast/continuous-delivery-office-hours/id1872101651">Apple Podcasts</a>, and <a href="https://pca.st/hwjaox59">Pocket Casts</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Configuring generic OIDC provider for authentication</title>
      <link href="https://octopus.com/blog/auth0-oidc-authentication-provider" />
      <id>https://octopus.com/blog/auth0-oidc-authentication-provider</id>
      <published>2026-10-05</published>
      <updated>2026-10-05</updated>
      <summary>Learn how to configure Auth0 as a generic OpenID Connect (OIDC) authentication provider for Octopus Deploy.</summary>
      <author>
        <name>Shawn Sesna, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>The use of OpenID Connect (OIDC) has gained popularity as a standard for authentication and authorization.  Octopus Deploy supports using OIDC for integrating with <a href="https://octopus.com/docs/packaging-applications/package-repositories">External Feeds</a>, <a href="https://octopus.com/docs/api/examples/accounts">Accounts</a>, and as an authentication provider.  In this post, I will cover how to configure an OIDC provider as an authentication mechanism for Octopus Deploy.  This post uses Auth0 as the OIDC provider.</p>
<h2 id="setting-up-auth0">Setting up Auth0</h2>
<p>To get started, you’ll need to configure an <code>Application</code> in Auth0.</p>
<h3 id="create-an-auth0-application">Create an Auth0 application</h3>
<p>Auth0 makes the process of creating an <code>Application</code> pretty painless:</p>
<ol>
<li>After logging in, expand the <strong>Applications</strong> category within the Auth0 navigation</li>
<li>Click on <strong>Applications</strong></li>
<li>Click <strong>Create Application</strong></li>
<li>Give the <code>Application</code> a name (eg., Octopus Deploy)</li>
<li>Choose <code>Regular Web Application</code></li>
<li>Click <strong>Create</strong></li>
</ol>
<h3 id="configure-the-auth0-application">Configure the Auth0 application</h3>
<p>Once the <code>Application</code> has been created, there are a couple of settings that need to be configured so it will work with Octopus Deploy:</p>
<ol>
<li>
<p>Navigate to the <strong>Settings</strong> tab on your newly created <code>Application</code></p>
</li>
<li>
<p>Scroll down to the <strong>Application URIs</strong> section</p>
</li>
<li>
<p>In the <strong>Allowed Callback URLs</strong>, enter the following: <code>https://&#x3C;your Octopus Deploy Server URL>/api/users/authenticatedToken/GenericOidc</code>, for example:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>https://shawnsesna.octopusdemos.app/api/users/authenticatedToken/GenericOidc</span></span></code></pre>
</li>
<li>
<p>(Optional) In the <strong>Allowed Logout URLs</strong> enter the following: <code>https://&#x3C;your Octopus Deploy Server URL>/app#/users/sign-out</code>, for example:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>https://shawnsesna.octopusdemos.app/app#/users/sign-out</span></span></code></pre>
</li>
<li>
<p>Click <strong>Save</strong></p>
</li>
</ol>
<p>Before navigating to Octopus Deploy, record the values of <code>Domain</code>, <code>Client ID</code>, and <code>Client Secret</code>.  These values are needed to configure OIDC authentication in Octopus Deploy.</p>
<h2 id="configure-openid-connect-in-octopus-deploy">Configure OpenID Connect in Octopus Deploy</h2>
<p>Configuration of OIDC authentication can be completed in just a few minutes:</p>
<ol>
<li>Navigate to <strong>Configuration</strong> (cog) in Octopus Deploy</li>
<li>Click on <strong>Settings</strong> -> <strong>OpenID Connect</strong></li>
<li>Enable the integration by ticking the <strong>Is Enabled</strong> checkbox</li>
<li>Set <strong>Username Claim Type</strong> to <code>name</code></li>
<li>Set <strong>Display Name</strong> to something meaningful (eg. <code>Auth0</code>)</li>
<li>Set <strong>Issuer</strong> to <code>https://&#x3C;Domain value from Auth0>/</code> <em>Note the trailing slash</em></li>
<li>Set <strong>Client ID</strong> to the <code>Client ID</code> value from Auth0</li>
<li>Set <strong>Client Secret</strong> to the <code>Client Secret</code> value from Auth0</li>
<li>(Optional) Tick <strong>Allow Auto User Creation</strong> to automatically create an Octopus user account when a new user logs in using OpenID Connect</li>
<li>Click <strong>Save</strong></li>
</ol>
<p>Configuration is now complete!  Users will now see the OpenID Connect login option when signing into Octopus Deploy.</p>
<figure>
<p><img src="/blog/i/x/auth0-oidc-authentication-provider/octopus-deploy-openid-connect-login.png" alt="Octopus Deploy login screen showing the OpenID Connect authentication option" srcset="/blog/i/600/auth0-oidc-authentication-provider/octopus-deploy-openid-connect-login.webp 360w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="360" height="644"></p>
</figure>
<h2 id="including-roles-from-auth0">Including roles from Auth0</h2>
<p>Unlike other providers such as Microsoft Entra or Okta, Auth0 does not include role assignments by default.  Instead, you must insert a step into the process after login, but before the token is issued.</p>
<h3 id="create-an-auth0-role">Create an Auth0 role</h3>
<p>If you don’t already have roles in Auth0, creating a role in Auth0 is a straightforward process:</p>
<ol>
<li>Expand the <code>User Management</code> section and click <code>Roles</code></li>
<li>Click the <strong>+ Create Role</strong> button</li>
<li>Give the role a <code>Name</code> and <code>Description</code></li>
<li>Click on the <strong>Users</strong> tab and add some users to the role</li>
</ol>
<h3 id="create-an-auth0-actions-trigger">Create an Auth0 Actions Trigger</h3>
<p>Auth0 provides a mechanism for inserting the role information into the OIDC token called an <code>Action</code>.  An <code>Action</code> can be added to a <code>Trigger</code> such as the <code>post-login</code>, which is executed after the user logs in, but before the token is issued.</p>
<ol>
<li>
<p>Expand the <code>Actions</code> section and click <code>Triggers</code></p>
</li>
<li>
<p>Under <strong>Sign Up &#x26; Login</strong>, choose the <code>post-login</code> trigger</p>
</li>
<li>
<p>The process will consist of two steps: <strong>Start</strong> and <strong>Complete</strong>.  Click on the <strong>+</strong> button next to <code>Add Action</code> and select <code>Create Custom Action</code></p>
</li>
<li>
<p>Give the <code>Action</code> a name (eg., Include Roles)</p>
</li>
<li>
<p>For this example, leave the <code>Runtime</code> as the recommended <code>Node 22</code> value</p>
</li>
<li>
<p>Update the code to the following</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#267F99">exports</span><span style="color:#000000">.</span><span style="color:#795E26">onExecutePostLogin</span><span style="color:#000000"> = </span><span style="color:#0000FF">async</span><span style="color:#000000"> (</span><span style="color:#001080">event</span><span style="color:#000000">, </span><span style="color:#001080">api</span><span style="color:#000000">) </span><span style="color:#0000FF">=></span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#0000FF">  const</span><span style="color:#0070C1"> namespace</span><span style="color:#000000"> = </span><span style="color:#A31515">'https://shawnsesna.octopusdemos.app'</span><span style="color:#000000">;</span></span>
<span class="line"><span style="color:#0000FF">  const</span><span style="color:#0070C1"> roles</span><span style="color:#000000"> = </span><span style="color:#001080">event</span><span style="color:#000000">.</span><span style="color:#001080">authorization</span><span style="color:#000000">?.</span><span style="color:#001080">roles</span><span style="color:#000000"> ?? </span><span style="color:#0000FF">null</span><span style="color:#000000">;</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">  api</span><span style="color:#000000">.</span><span style="color:#001080">idToken</span><span style="color:#000000">.</span><span style="color:#795E26">setCustomClaim</span><span style="color:#000000">(</span><span style="color:#A31515">`</span><span style="color:#0000FF">${</span><span style="color:#001080">namespace</span><span style="color:#0000FF">}</span><span style="color:#A31515">/roles`</span><span style="color:#000000">, </span><span style="color:#001080">roles</span><span style="color:#000000"> ?? []);</span></span>
<span class="line"><span style="color:#000000">};</span></span></code></pre>
<p>This code inserts a JSON array payload containing assigned roles into the issued token.  In this case, <code>${namespace}/roles</code> acts as the key with the <code>roles</code> array as the value.  The <code>namespace</code> value is important, as this is what Octopus needs to determine where to look for role assignments.  In the above example, the <code>namespace</code> value is <code>https://shawnsesna.octopusdemos.app/roles</code>.  The <code>namespace</code> can be whatever you want; it doesn’t have to be the URL to your Octopus instance.</p>
</li>
<li>
<p>Click <strong>Deploy</strong> to save the <code>Action</code> to the <code>Library</code>.</p>
</li>
<li>
<p>Navigate back to <code>Triggers</code></p>
</li>
<li>
<p>Under <strong>Sign Up &#x26; Login</strong>, choose the <code>post-login</code> trigger</p>
</li>
<li>
<p>Your newly created <code>Action</code> will now be listed; click and drag the <code>Action</code> and drop it between <strong>Start</strong> and <strong>Complete</strong></p>
</li>
<li>
<p>Click <strong>Apply</strong></p>
</li>
</ol>
<p>Auth0 is now configured to include role assignments with the OIDC token.  This will allow you to add an Auth0 role as an external role to an Octopus Team.</p>
<h3 id="update-the-openid-connect-octopus-configuration">Update the OpenID Connect Octopus configuration</h3>
<p>Now that roles are included within the token, you’ll need to update the OpenID Connect configuration in Octopus to be able to “see” them.</p>
<ol>
<li>Navigate to <strong>Configuration</strong> (cog) in Octopus Deploy</li>
<li>Click on <strong>Settings</strong> -> <strong>OpenID Connect</strong></li>
<li>Update <strong>Role Claim Type</strong> to the key from the JSON.  In the above example, this value is <code>https://shawnsesna.octopusdemos.app/roles</code></li>
<li>Click Save</li>
</ol>
<p>Octopus now knows where in the token to locate role membership.  Assigning the name of the role to an Octopus Deploy Team will grant anyone who has the role within their token access to the resources that the Team has been granted, removing the need to add individual users to the Team.</p>
<h3 id="troubleshooting-roles">Troubleshooting roles</h3>
<p>OIDC token exchange takes place server-to-server, making the ability to see what was included in the JSON Web Token (JWT) difficult.  Fortunately, there is an easy way to pass the token to jwt.io for debugging purposes.</p>
<ol>
<li>In Auth0, navigate to your <code>Application</code></li>
<li>Scroll to the <strong>Applications URIs</strong> section</li>
<li>Add <code>https://jwt.io</code> to the <strong>Allowed Callback URLs</strong></li>
<li>Open a new browser window (Incognito/InPrivate is best to ensure cache doesn’t interfere) and go to:</li>
</ol>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>https://&#x3C;Your Auth0 domain>/authorize?client_id=&#x3C;Your Client ID>&#x26;response_type=id_token&#x26;redirect_uri=https://jwt.io&#x26;scope=openid%20profile%20email&#x26;nonce=12345</span></span></code></pre>
<p>You will be prompted to log in to Auth0, then redirected to <code>https://jwt.io</code>.  Jwt.io will automatically decode the returned JWT so you can see what is included.  Below is an example:</p>
<p>Decoded payload</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#0451A5">  "https://shawnsesna.octopusdemos.app/roles"</span><span style="color:#000000">: [</span></span>
<span class="line"><span style="color:#A31515">    "Demo-Argo-CD-Apps"</span></span>
<span class="line"><span style="color:#000000">  ],</span></span>
<span class="line"><span style="color:#0451A5">  "nickname"</span><span style="color:#000000">: </span><span style="color:#A31515">"joe.dirt"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "name"</span><span style="color:#000000">: </span><span style="color:#A31515">"joe.dirt@fake.com"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "picture"</span><span style="color:#000000">: </span><span style="color:#A31515">"https://s.gravatar.com/avatar/30daab2c90a676d8ab323d0eab8c8f45?s=480&#x26;r=pg&#x26;d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fjo.png"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "updated_at"</span><span style="color:#000000">: </span><span style="color:#A31515">"2026-09-17T19:04:13.380Z"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "email"</span><span style="color:#000000">: </span><span style="color:#A31515">"joe.dirt@fake.com"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "email_verified"</span><span style="color:#000000">: </span><span style="color:#0000FF">true</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "iss"</span><span style="color:#000000">: </span><span style="color:#A31515">"https://[Your Auth0 domain]/"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "aud"</span><span style="color:#000000">: </span><span style="color:#A31515">"&#x3C;Your Client ID>"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "sub"</span><span style="color:#000000">: </span><span style="color:#A31515">"&#x3C;Your User ID>"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "iat"</span><span style="color:#000000">: </span><span style="color:#098658">1789671854</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "exp"</span><span style="color:#000000">: </span><span style="color:#098658">1789707854</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "sid"</span><span style="color:#000000">: </span><span style="color:#A31515">"&#x3C;Your SID>"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "nonce"</span><span style="color:#000000">: </span><span style="color:#A31515">"12345"</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<p>From our example, you can see that the key <code>https://shawnsesna.octopusdemos.app</code> has been assigned the value of the roles the user <code>joe.dirt@fake.com</code> belongs to: Demo-Argo-CD-Apps.  The user <code>joe.dirt@fake.com</code> will gain access to any resource where the Octopus Deploy Team has included the external role Demo-Argo-CD-Apps.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post covered how to add Auth0 as a generic OpenID Connect authentication provider for Octopus Deploy.  Some of the steps were specific to Auth0; however, the overall instructions will work for any OpenID Connect provider.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Build your own Octopus dashboards with llms.txt and an AI coding agent</title>
      <link href="https://octopus.com/blog/build-your-own-octopus-dashboards-with-llms-txt" />
      <id>https://octopus.com/blog/build-your-own-octopus-dashboards-with-llms-txt</id>
      <published>2026-10-02</published>
      <updated>2026-10-02</updated>
      <summary>See how I used Octopus Server's llms.txt file and an AI coding agent to create realistic test data and build 3 custom dashboards with the Octopus API.</summary>
      <author>
        <name>Egor Pavlikhin, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>The built-in Octopus dashboard shows you which releases are deployed to each environment. When you manage deployments across many tenants and teams, you might want a more focused view. Which tenants are 2 releases behind? Which approvals are holding up the Payments team? How far has a release progressed across environments and tenants?</p>
<p>You can build these views with the Octopus REST API, but first you need to understand its endpoints, request bodies, and deployment step properties. Octopus’s <code>llms.txt</code> file brings that information together in a format AI coding agents can read and understand.</p>
<p>For this blog post I gave the file to a coding agent and asked it to:</p>
<ol>
<li>Create realistic test data for a multi-tenant company with several teams</li>
<li>Build custom dashboards for those teams</li>
<li>Visualize release progress in a new and interesting way</li>
</ol>
<p>Everything below ran against a local Octopus install. I used Claude Code, gave it the URL of the <code>llms.txt</code> file, an <a href="https://octopus.com/docs/api/authentication/create-an-api-key/#creating-an-agent-api-key">agent API key</a>, and the prompts quoted throughout this post.</p>
<h2 id="what-is-llmstxt">What is llms.txt</h2>
<p><a href="https://llmstxt.org/">llms.txt</a> is a convention for publishing a plain Markdown file at <code>/llms.txt</code> that condenses information about a website or a collection of documents into a single file. This can also work similarly for API documentation. In essence, it fills the same role for agents that a well-written README does for people.</p>
<p>Octopus serves its own file at <code>https://your-octopus-server/llms.txt</code>. As of the time of this writing the file is about 400 KB of Markdown and has 5 sections:</p>
<div class="table-wrap">




























<table><thead><tr><th>Section</th><th>What it contains</th></tr></thead><tbody><tr><td>Authentication</td><td>API keys, the <code>X-Octopus-ApiKey</code> header, cookie login, and cross-site request forgery (CSRF) protection</td></tr><tr><td>Space selection</td><td>The 2 URL prefix forms, <code>/api/Spaces-1/...</code> and <code>/api/spaces/default/...</code></td></tr><tr><td>Endpoints</td><td>Documented routes grouped by resource, with query parameters, request body type, and response type</td></tr><tr><td>Types</td><td>One line per type listing its properties and their child types</td></tr><tr><td>Steps and their properties</td><td>Every <code>ActionType</code> with its <code>Octopus.Action.*</code> property keys, defaults, and allowed values</td></tr></tbody></table></div>
<p>A typical endpoint line then looks like this:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>- `POST /deployments` - Create a Deployment | Prefixes (pick one): `/{spaceId}`, `/spaces/{spaceIdentifier}` | Body: CreateDeploymentCommand → DeploymentResource</span></span></code></pre>
<p>And the matching type line describing the underlying types:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>CreateDeploymentCommand: SpaceId (string), ReleaseId (string), ChannelId (string), DeploymentProcessId (string), EnvironmentId (string), TenantId (string), ForcePackageDownload (boolean), ...</span></span></code></pre>
<p>Together, these lines give the agent an endpoint and the fields for its request body. It can search for <code>POST /deployments</code>, find the matching type definition, and construct a request.</p>
<p>The Steps section is particularly useful. To build a deployment process through the API, for example, the agent needs to know that a script step uses the <code>ActionType</code> value <code>Octopus.Script</code> and stores its script in <code>Octopus.Action.Script.ScriptBody</code>. The Steps section provides these property keys alongside the API information derived from our existing Swagger docs.</p>
<p>Why is this useful? The <code>llms.txt</code> file gives agents a succinct overview of everything available in the Octopus API, making it easy to build:</p>
<ul>
<li>Custom views ands dashboards</li>
<li>Automation scripts</li>
<li>Custom integrations with Octopus</li>
</ul>
<h2 id="part-1-seeding-realistic-data">Part 1: Seeding realistic data</h2>
<p>First, I wanted to fill my Octopus instance with data. I started with an almost empty space: one project, 3 environments, no tenants, no targets. The prompt was:</p>
<blockquote>
<p>Use the Octopus API to create test data. Projects with deployment processes, fake targets, project variables, tenants, and anything else you can think of. Make the projects look realistic.</p>
</blockquote>
<p>The agent fetched <code>llms.txt</code>, searched the sections it needed, and wrote a small Python helper for calling the API securely (to avoid pasting the API key in the agent’s logs):</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#0000FF">def</span><span style="color:#795E26"> call</span><span style="color:#000000">(</span><span style="color:#001080">method</span><span style="color:#000000">, </span><span style="color:#001080">path</span><span style="color:#000000">, </span><span style="color:#001080">body</span><span style="color:#000000">=</span><span style="color:#0000FF">None</span><span style="color:#000000">):</span></span>
<span class="line"><span style="color:#000000">    req = urllib.request.Request(BASE + </span><span style="color:#A31515">"/"</span><span style="color:#000000"> + SPACE + path, </span><span style="color:#001080">data</span><span style="color:#000000">=json.dumps(body).encode() </span><span style="color:#AF00DB">if</span><span style="color:#000000"> body </span><span style="color:#AF00DB">else</span><span style="color:#0000FF"> None</span><span style="color:#000000">, </span><span style="color:#001080">method</span><span style="color:#000000">=method)</span></span>
<span class="line"><span style="color:#000000">    req.add_header(</span><span style="color:#A31515">"X-Octopus-ApiKey"</span><span style="color:#000000">, KEY)</span></span>
<span class="line"><span style="color:#000000">    req.add_header(</span><span style="color:#A31515">"Content-Type"</span><span style="color:#000000">, </span><span style="color:#A31515">"application/json"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#AF00DB">    with</span><span style="color:#000000"> urllib.request.urlopen(req) </span><span style="color:#AF00DB">as</span><span style="color:#000000"> r:</span></span>
<span class="line"><span style="color:#AF00DB">        return</span><span style="color:#000000"> json.loads(r.read().decode())</span></span></code></pre>
<p>The agent created a fictional company called Harborline, with 5 teams running a platform for retailers. Each retailer is represented as a separate tenant. It then seeded the instance with the deployment processes, runbooks, variables, deployment targets, etc.</p>
<h3 id="how-the-llmstxt-file-helped">How the llms.txt file helped</h3>
<p>Here are some examples of where <code>llms.txt</code> was useful for my agent:</p>
<ul>
<li><strong>Space prefixes.</strong> The agent could understand the Spaces scoping concept and apply the correct URL prefixes to the API calls, like <code>/api/Spaces-1/...</code>.</li>
<li><strong>Deployment process authoring.</strong> Keyed step properties like <code>Octopus.Action.TargetRoles</code>, <code>Octopus.Action.RunOnServer</code>, and <code>Octopus.Action.Manual.ResponsibleTeamIds</code> were documented in the file and the agent used those to create the deployment processes.</li>
<li><strong>Backdating releases.</strong> <code>CreateReleaseCommand</code> has an optional <code>Assembled</code> field. The agent used it to spread 31 releases across August and September so the dashboards would have history to show.</li>
<li><strong>Breadth of the API.</strong> As Octopus Deploy is an API-first product, the agent was able to understand what Octopus is and what it offers just by reading the available API endpoints.</li>
</ul>
<p>While the text file gave the agent enough ground to start, there were still issues where it had to make requests to the API first to understand the right shape of things. The agent worked through these issues by making API requests and reading back the detailed error and validation messages.</p>
<h2 id="part-2-custom-dashboards">Part 2: Custom dashboards</h2>
<p>One of the best scenarios for using the Octopus REST API directly is creating customized dashboards tailored to your specific requirements. This is much easier to achieve now that you can just point your coding agent to the <code>llms.txt</code> and ask it to build it for you.</p>
<h3 id="keep-the-api-key-out-of-the-browser">Keep the API key out of the browser</h3>
<p>To avoid embedding your API key into the dashboard, you can use a small proxy script. The proxy serves the static HTML and forwards requests under <code>/api/</code> to Octopus, adding the API key header. This gives the browser a single origin for both the page and its API requests, solving Cross-Origin Resource Sharing (CORS) issues.</p>
<p>Here’s is an example that forwards a request:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#0000FF">def</span><span style="color:#795E26"> proxy</span><span style="color:#000000">(</span><span style="color:#001080">self</span><span style="color:#000000">):</span></span>
<span class="line"><span style="color:#000000">    req = urllib.request.Request(OCTOPUS_URL + </span><span style="color:#0000FF">self</span><span style="color:#000000">.path)</span></span>
<span class="line"><span style="color:#000000">    req.add_header(</span><span style="color:#A31515">"X-Octopus-ApiKey"</span><span style="color:#000000">, API_KEY)</span></span>
<span class="line"><span style="color:#AF00DB">    with</span><span style="color:#000000"> urllib.request.urlopen(req) </span><span style="color:#AF00DB">as</span><span style="color:#000000"> r:</span></span>
<span class="line"><span style="color:#000000">        body = r.read()</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.send_response(</span><span style="color:#098658">200</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.send_header(</span><span style="color:#A31515">"Content-Type"</span><span style="color:#000000">, </span><span style="color:#A31515">"application/json"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.end_headers()</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.wfile.write(body)</span></span></code></pre>
<p>Now here are some examples of the dashboards you could build.</p>
<h3 id="the-tenant-rollout-board">The tenant rollout board</h3>
<p>The first dashboard is a matrix. Tenants are rows, grouped by any tenant tag set. Projects are columns, grouped by team. Each cell shows the release a tenant is running in the selected environment, and how many releases behind the newest one it is.</p>
<figure><p><img src="/blog/i/x/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.png" alt="Tenant rollout board grouped by region, showing release versions, deployment states, and how far tenants are behind" loading="lazy" srcset="/blog/i/600/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.webp 600w, /blog/i/1000/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.webp 1000w, /blog/i/2000/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.webp 1485w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1485" height="1071"></p></figure>
<p>The board uses these endpoints:</p>
<div class="table-wrap">




























<table><thead><tr><th>Endpoint</th><th>Used for</th></tr></thead><tbody><tr><td><code>GET /dashboard?showAll=true</code></td><td>Current deployment per project, environment, and tenant, plus projects, tenants, and environments in one call</td></tr><tr><td><code>GET /projects/{id}/releases</code></td><td>The newest release per project and per channel, to work out how far behind a tenant is</td></tr><tr><td><code>GET /tagsets/all?scopes=Tenant</code></td><td>Tag sets for grouping and filtering</td></tr><tr><td><code>GET /tenants/variables-missing</code></td><td>Tenants that cannot be deployed to because a required variable has no value</td></tr><tr><td><code>GET /interruptions?pendingOnly=true</code></td><td>Deployments paused for approval</td></tr></tbody></table></div>
<p>The board calculates how far behind a tenant is in its channel, such that a tenant on the latest hotfix isn’t flagged as behind the mainline.</p>
<p>The summary tiles highlight where a release manager needs to act. In this example, 51 tenant deployments are behind the latest release in their channel, and 5 tenants have missing variables. Selecting <strong>Only tenants with a problem</strong> narrows the board to the affected tenants.</p>
<h3 id="the-team-health-board">The team health board</h3>
<p>The second dashboard groups work by team, with one card for each. Each card starts with deployments waiting for approval and tasks that failed in the last 24 hours. Every item links to its task in Octopus, so you can go back to the Octopus UI at any point.</p>
<p>Below these items, a table shows each project’s releases across environments. Version labels like <code>2026.3.3 ×5</code> summarize tenanted deployments. The footer shows target health by environment, and a banner at the top of the dashboard shows active or upcoming deployment freezes.</p>
<figure><p><img src="/blog/i/x/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.png" alt="Team health board showing each team&#x27;s pending approvals, recent failures, and project versions across environments" loading="lazy" srcset="/blog/i/600/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.webp 600w, /blog/i/1000/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.webp 1000w, /blog/i/2000/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.webp 1485w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1485" height="1335"></p></figure>
<div class="table-wrap">
































<table><thead><tr><th>Endpoint</th><th>Used for</th></tr></thead><tbody><tr><td><code>GET /dashboard?showAll=true</code></td><td>Current deployments and the project group for each project</td></tr><tr><td><code>GET /interruptions?pendingOnly=true</code></td><td>Pending approvals</td></tr><tr><td><code>GET /tasks?states=Failed,TimedOut&#x26;fromCompletedDate=...</code></td><td>Failures in the last 24 hours</td></tr><tr><td><code>GET /machines/all</code></td><td>Target health and deactivated targets</td></tr><tr><td><code>GET /teams/all</code></td><td>Mapping project groups to the team that owns them</td></tr><tr><td><code>GET /deploymentfreezes</code></td><td>Active and upcoming freezes</td></tr></tbody></table></div>
<p>The agent sorted the cards by an attention score: 3 points per failure, 2 per pending approval. The team with the highest score appears at the top left.</p>
<h2 id="part-3-a-release-flow-diagram">Part 3: A release flow diagram</h2>
<p>And for the last one, just for fun, I asked the agent:</p>
<blockquote>
<p>Use the API via llms.txt to create one other custom dynamic HTML representation of Octopus data in a creative way, perhaps a visualization.</p>
</blockquote>
<p>The agent chose a Sankey diagram, which uses ribbons to show connections between groups. Here, the ribbons connect a project’s releases to their current environments and the tenants or tenant groups they serve.</p>
<p>Ribbon width represents the number of tenants. You can color the ribbons by release to follow a version across the diagram, or by deployment state to highlight failures and pending approvals.</p>
<figure><p><img src="/blog/i/x/build-your-own-octopus-dashboards-with-llms-txt/release-flow.png" alt="Release flow diagram connecting Storefront Web releases to environments and tenant regions" loading="lazy" srcset="/blog/i/600/build-your-own-octopus-dashboards-with-llms-txt/release-flow.webp 600w, /blog/i/1000/build-your-own-octopus-dashboards-with-llms-txt/release-flow.webp 1000w, /blog/i/2000/build-your-own-octopus-dashboards-with-llms-txt/release-flow.webp 1500w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1500" height="1000"></p></figure>
<p>With this you could easily create interactive charts that help you analyze a complex matrix of deployments to spot trends and anomalies.</p>
<h2 id="try-it-yourself">Try it yourself</h2>
<p>You will need a publicly accessible instance of Octopus, an <a href="https://octopus.com/docs/api/authentication/create-an-api-key/#creating-an-agent-api-key">agent API key</a>, and a coding agent that can fetch URLs and run scripts.</p>
<ol>
<li>Open <code>https://your-octopus-server/llms.txt</code> (or <code>https://your-octopus-server/api/experimental/llms.txt</code>) in a browser to confirm your version serves it.</li>
<li>Give your agent the URL and an API key with permissions scoped to a test space. Ask it to fetch the file before doing anything else.</li>
<li>Start with a read-only task, like “list every tenant that is more than one release behind in Production.” Check the answer against the portal.</li>
<li>Move on to writing the real code. When the agent makes a mistake, the API response will explain why. Use this loop to build the functionality you want.</li>
</ol>
<p>Keep the API key on the server side. If you build dashboards, create a service account, and put a proxy between the browser and Octopus, like the one above. Never paste the key into the page source. If you are using an external authentication provider, you could also extend this to use a proper authentication mechanism rather than just an API key, though the implementation could become quite complex.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>The Future of Platform Engineering report</title>
      <link href="https://octopus.com/blog/future-of-platform-engineering" />
      <id>https://octopus.com/blog/future-of-platform-engineering</id>
      <published>2026-10-01</published>
      <updated>2026-10-01</updated>
      <summary>How organizations are evolving Platform Engineering, what's working, what isn't, and why</summary>
      <author>
        <name>Charlotte Fleming, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>We're excited to announce the release of the <a href="https://octopus.com/publications/future-of-platform-engineering-report">2026 Future of Platform Engineering report</a>. The report examines why organizations adopt platforms, what shapes their decisions along the way, and what high-performing platform teams do differently.</p>
<p>Platform Engineering has moved from a frontier practice to a mainstream industry standard. It's now an established discipline with recognizable patterns and a maturing body of evidence. The report draws on primary survey data from 379 technical platform practitioners worldwide, and a review of the current literature to establish where the discipline now stands.</p>
<p>A consistent theme runs through the findings, separating platforms that succeed from those that struggle. The decisions and practices around the technology determine success: how adoption is managed, how clearly the strategy is set, and how engaged the leadership is.</p>
<h2>Why teams actually build platforms</h2>
<p>Organizations adopt platforms primarily for automation and efficiency purposes. Developer experience is still a consideration, but less of a priority for most. When they explain why the initiative was funded, automation comes first. This was a consistent finding in our previous report, the <a href="https://octopus.com/publications/platform-engineering-pulse">2025 Platform Engineering Pulse report</a>.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/benefits-reasons.png" alt="Top 10 reasons for adopting platforms" loading="lazy" }
:figcaption[Top 10 reasons for adopting platforms (multi-select)]</p>
<p>:::</p>
<p>The motivation behind platform adoption is important as it shapes how success is measured. A platform funded for efficiency reasons and later judged on developer experience will look like a failure that isn't one. Teams that name their primary motivation early have a clearer basis for knowing whether the platform worked.</p>
<h2>The organizational constraint to success is strategy</h2>
<p>Struggling teams most often report a lack of clear strategy as their obstacle, ahead of the technical complexity of building a platform.</p>
<p>Organizations that predominantly build their platform features from scratch are also the most likely to report this gap. Of respondents building from scratch, 57.9% also cite a lack of a clear strategy as an obstacle. While this may suggest that teams are building without a plan, many engineers build to work out a problem, which makes the build a method rather than a problem. This would explain why these two often turn up together.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/obstacles-build-buy.png" alt="Heatmap of obstacles by dominant build approach" loading="lazy" }
:figcaption[Build-versus-buy feature implementation due to a lack of a clear strategy and technical complexity. Heatmap showing the percentage of respondents selecting the obstacle by dominant build approach.]</p>
<p>:::</p>
<h2>A result that caught us off guard</h2>
<p>Among teams reporting mandatory adoption, 62.2% achieved most of their goals (high-performers). For respondents reporting optional platform adoption, this sits at roughly 27.5%.</p>
<p>This was an unexpected finding. Industry advice that comes up again and again in Platform Engineering is, don't force adoption, treat the platform like a product, and let it earn its users. We assumed that optional adoption would track more with high-performing teams and strong outcomes, but we found the opposite.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/goals-adoption-strategy.png" alt="Goal delivery by adoption strategy" loading="lazy" }
:figcaption[Goal delivery by adoption strategy]</p>
<p>:::</p>
<p>The tension here is real, but it doesn't mean that everyone else is doing it wrong. It's two different questions getting two different answers. Mandatory adoption is the stronger signal for whether a platform delivers on the goals it set. Optional adoption speaks to something else entirely: a platform that earns its place because developers choose it rather than because they're asked to, which is a success worth having in its own right.</p>
<p>Mandatory adoption also doesn't move on its own. In the network analysis, it clusters with stronger leadership direction, higher job satisfaction, and greater confidence in budget safety, and those measures correlate with one another about as much as each correlates with goal delivery. Mandatory adoption without the leadership engagement and budget security is unlikely to produce the same result. What appears as a decision about adoption is usually a decision about how much of the organization is behind the platform.</p>
<h2>Who's actually getting value from AI?</h2>
<p>Teams reporting the most positive impacts from AI were the platforms offering features we classified as advanced, like <strong>ephemeral environments</strong>, <strong>cost control</strong>, and <strong>code coverage</strong>. Adopting these features actually shifted platforms from no AI impact to a positive AI impact on software delivery speed and stability. The difference was in which capabilities a platform had, not how many it had.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/ai-impact-features-software.png" alt="Features ranked by AI impact on software delivery speed and stability" loading="lazy" }
:figcaption[Features ranked by AI impact on software delivery speed and stability]</p>
<p>:::</p>
<p>That is consistent with how <a href="https://dora.dev/research/2025/dora-report/">DORA described AI in 2025</a>, as an amplifier of the system it enters. Where the foundations are solid, the gains compound, and where they aren't, the friction is amplified instead. Teams on mature, well-designed platforms get more out of AI than teams with ad hoc setups.</p>
<h2>What this means for platforms</h2>
<p>The patterns associated with successful platform teams emerged in organizational decisions: how clearly the strategy is set, how engaged the leadership is, and how adoption is handled. This is an encouraging result. The platforms that deliver well are the ones where people agreed what the platform was for and committed to it.</p>
<p>Platform Engineering is maturing, and the patterns are becoming clear enough to learn from. <a href="https://octopus.com/publications/future-of-platform-engineering-report">Read the report</a> for the full analysis, including how teams measure success and what high-performing teams do differently.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Argo CD Rollouts</title>
      <link href="https://octopus.com/blog/octo-easy-mode-24-argocd-rollouts" />
      <id>https://octopus.com/blog/octo-easy-mode-24-argocd-rollouts</id>
      <published>2026-09-30</published>
      <updated>2026-09-30</updated>
      <summary>Learn how to deploy Argo CD Rollouts</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Argo CD Rollouts provides advanced deployment capabilities for Kubernetes, including blue-green, canary, canary analysis, experimentation, and progressive delivery. In this post, you’ll create a sample project that demonstrates how to deploy Argo CD Rollouts using Octopus.</p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<p><a href="https://www.youtube.com/watch?v=9id8hQaogGA">Easy Mode - Argo CD Rollouts</a></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>An <a href="https://octopus.com/start">Octopus Cloud</a> account. If you don’t have one, you can sign up for a free trial.</li>
<li>The Octopus AI Assistant Chrome extension. You can install it from the <a href="https://chromewebstore.google.com/detail/octopus-ai-assistant/agfpjjibnieiihjoehophlbamcifdfha">Chrome Web Store</a>.</li>
</ul>
<div class="hint"><p>The Octopus AI Assistant will work with an on-premises Octopus instance, but it requires more configuration. The
cloud-hosted version of Octopus doesn’t need extra configuration. This means the cloud-hosted version is the easiest way to get started.</p></div>
<h2 id="creating-the-project">Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create an Argo CD Rollout project called "24. Argo CD Rollouts"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#800000;font-weight:bold">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create a token account called "Mock Token".</span></span>
<span class="line"></span>
<span class="line"><span style="color:#800000;font-weight:bold">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create a feed named "Docker Hub" that points to "https://index.docker.io" using anonymous authentication.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#800000;font-weight:bold">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create a Kubernetes target called "Rollouts" with the tag "Kubernetes", the URL "https://mockk8s.octopusdemos.com", using the health check container image "octopusdeploy/worker-tools:6.5.0-ubuntu.22.04" from the "Docker Hub" feed, using the token account "Mock Token", and the "Hosted Ubuntu" worker pool. Link the target to the "Development", "Prod 10", "Prod 50", and "Prod 100" environments.</span></span></code></pre>
<p>The project <code>24. Argo CD Rollouts</code> will be created with a Kubernetes target pointing to a mock Kubernetes cluster and a Git Connection providing the credentials for the mock Git server.</p>
<p>The project references a lifecycle called <code>Progressive</code>, which has the following phases (with each phase having a single environment of the same name):</p>
<ul>
<li><code>Development</code></li>
<li><code>Prod 10</code></li>
<li><code>Prod 50</code></li>
<li><code>Prod 100</code></li>
</ul>
<p>The Argo CD Rollout manifest, sourced from <a href="https://mockgit.octopusdemos.com/browse/bloguser/argorollout/template/rollout.yaml?branch=main">the mock Git repo</a>, will be deployed to the mock Kubernetes cluster to simulate a canary deployment with production traffic for the new revision, starting at 10%, increasing to 50%, and then finally 100% of the traffic.</p>
<p>Deploying an Argo CD Rollout manifest to multiple environments requires some planning:</p>
<ul>
<li>The non-production environment, <code>Development</code>, deploys a new revision of the manifest (either to a non-production namespace like <code>development</code> or to a non-production cluster) and has 100% of the traffic going to the new revision with no advanced deployment patterns. This allows teams to test their changes without waiting for all traffic to cut over.</li>
<li>The first production environment, <code>Prod 10</code>, representing 10% of traffic, also deploys a new revision of the manifest (to a production namespace like <code>prod</code> or to a production cluster) and directs 10% of the traffic to the new revision.</li>
<li>Subsequent production environment deployments, <code>Prod 50</code> and <code>Prod 100</code>, then promote the new revision to 50% and then finally 100% of the traffic by calling <code>kubectl argo rollouts promote rollouts-demo</code>.</li>
</ul>
<p>These patterns are modelled in the Argo CD Rollout manifest below, which uses <a href="https://octopus.com/docs/projects/variables/variable-substitutions#run-conditions">Octopus template syntax</a> to define unique canary rules for the <code>Development</code> environment and the production environments. Also note that the <code>template.spec.containers[0].image</code> property is set to the Octopus project variable <code>#{Project.Image}</code>. The <code>#{Project.Image}</code> variable resolves a second variable <code>#{Octopus.Action.Package[rollouts-demo].Image}</code>, which in turn references the image selected for the release:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#800000">apiVersion</span><span style="color:#000000">: </span><span style="color:#0000FF">argoproj.io/v1alpha1</span></span>
<span class="line"><span style="color:#800000">kind</span><span style="color:#000000">: </span><span style="color:#0000FF">Rollout</span></span>
<span class="line"><span style="color:#800000">metadata</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">  name</span><span style="color:#000000">: </span><span style="color:#0000FF">rollouts-demo</span></span>
<span class="line"><span style="color:#800000">spec</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">  replicas</span><span style="color:#000000">: </span><span style="color:#098658">5</span></span>
<span class="line"><span style="color:#800000">  strategy</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">    canary</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">      steps</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#008000">        #{if Octopus.Environment.Name == "Development"}</span></span>
<span class="line"><span style="color:#008000">        # Development environment deploys 100% of the traffic to the new version immediately</span></span>
<span class="line"><span style="color:#000000">        - </span><span style="color:#800000">setWeight</span><span style="color:#000000">: </span><span style="color:#098658">100</span></span>
<span class="line"><span style="color:#008000">        #{/if}</span></span>
<span class="line"><span style="color:#008000">        #{unless Octopus.Environment.Name == "Development"}</span></span>
<span class="line"><span style="color:#008000">        # In non-development environments, we will deploy 10% of the traffic to the new version,</span></span>
<span class="line"><span style="color:#008000">        # then pause for manual approval, then deploy 50% of the traffic to the new version,</span></span>
<span class="line"><span style="color:#008000">        # then pause for manual approval, and finally deploy 100% of the traffic to the new version.</span></span>
<span class="line"><span style="color:#000000">        - </span><span style="color:#800000">setWeight</span><span style="color:#000000">: </span><span style="color:#098658">10</span></span>
<span class="line"><span style="color:#000000">        - </span><span style="color:#800000">pause</span><span style="color:#000000">: {}</span></span>
<span class="line"><span style="color:#000000">        - </span><span style="color:#800000">setWeight</span><span style="color:#000000">: </span><span style="color:#098658">50</span></span>
<span class="line"><span style="color:#000000">        - </span><span style="color:#800000">pause</span><span style="color:#000000">: {}</span></span>
<span class="line"><span style="color:#008000">        #{/unless}</span></span>
<span class="line"><span style="color:#800000">  revisionHistoryLimit</span><span style="color:#000000">: </span><span style="color:#098658">2</span></span>
<span class="line"><span style="color:#800000">  selector</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">    matchLabels</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">      app</span><span style="color:#000000">: </span><span style="color:#0000FF">rollouts-demo</span></span>
<span class="line"><span style="color:#800000">  template</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">    metadata</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">      labels</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">        app</span><span style="color:#000000">: </span><span style="color:#0000FF">rollouts-demo</span></span>
<span class="line"><span style="color:#800000">    spec</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">      containers</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#000000">        - </span><span style="color:#800000">name</span><span style="color:#000000">: </span><span style="color:#0000FF">rollouts-demo</span></span>
<span class="line"><span style="color:#800000">          image</span><span style="color:#000000">: </span><span style="color:#A31515">"#{Project.Image}"</span></span>
<span class="line"><span style="color:#800000">          ports</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#000000">            - </span><span style="color:#800000">name</span><span style="color:#000000">: </span><span style="color:#0000FF">http</span></span>
<span class="line"><span style="color:#800000">              containerPort</span><span style="color:#000000">: </span><span style="color:#098658">8080</span></span>
<span class="line"><span style="color:#800000">              protocol</span><span style="color:#000000">: </span><span style="color:#0000FF">TCP</span></span>
<span class="line"><span style="color:#800000">          resources</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">            requests</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">              memory</span><span style="color:#000000">: </span><span style="color:#0000FF">32Mi</span></span>
<span class="line"><span style="color:#800000">              cpu</span><span style="color:#000000">: </span><span style="color:#0000FF">5m</span></span></code></pre>
<p>The end result is that new releases reference new image tags, creating new revisions of the rollout manifest. Traffic is then gradually shifted to the new application revision in production, using deployments to the environments <code>Prod 10</code>, <code>Prod 50</code>, and <code>Prod 100</code> to control each stage of the rollout.</p>
<p>The dashboard visualizes the rollout progress, allowing teams to quickly see the state of the environments:</p>
<p><img src="/blog/i/x/octo-easy-mode-24-argocd-rollouts/rollouts-dashboard.png" alt="Rollouts Dashboard" loading="lazy" srcset="/blog/i/600/octo-easy-mode-24-argocd-rollouts/rollouts-dashboard.webp 600w, /blog/i/1000/octo-easy-mode-24-argocd-rollouts/rollouts-dashboard.webp 1000w, /blog/i/2000/octo-easy-mode-24-argocd-rollouts/rollouts-dashboard.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></p>
<h2 id="what-just-happened">What just happened?</h2>
<p>You created a sample project with:</p>
<ul>
<li>Steps that deploy an Argo CD Rollout manifest to a mock Kubernetes cluster using the <code>Deploy a Kubernetes Manifest</code> step type.</li>
<li>Steps that promote the Argo CD Rollout to 50% and 100% of the traffic in production using the <code>Run a kubectl script</code> step type.</li>
<li>A manifest that uses Octopus template syntax to define unique canary rules for the development and production environments.</li>
</ul>
<h2 id="next-steps">Next steps</h2>
<p>To productionize the sample project, you can:</p>
<ul>
<li>Add integration or end-to-end tests after the manifest has been deployed or the traffic has been increased.</li>
<li>Abort a rollout if integration tests fail by calling <code>kubectl argo rollouts abort rollouts-demo</code> in a <code>Run a kubectl script</code> step set to run if the previous step fails.</li>
</ul>]]></content>
    </entry>
    <entry>
      <title>Octopus Approvals</title>
      <link href="https://octopus.com/blog/octopus-approvals" />
      <id>https://octopus.com/blog/octopus-approvals</id>
      <published>2026-09-29</published>
      <updated>2026-09-29</updated>
      <summary>Use approvals to gate deployments to sensitive production environments, prevent changes to QA environments while they're in use, or block the person who created a deployment from approving it themselves.</summary>
      <author>
        <name>Michelle O'Brien, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Octopus Approvals is a lightweight approval workflow that lets users gate deployments to environments that are sensitive to change. For customers that can’t justify the cost of a full ITSM implementation, it provides approval gating to ensure the right people review before a deployment progresses to the specified environments. This allows deployments to progress safely and satisfies standard compliance requirements.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approvals-main.png" alt="Home page for Octopus approvals showing two change requests waiting approval" loading="lazy" srcset="/blog/i/600/octopus-approvals/approvals-main.webp 600w, /blog/i/1000/octopus-approvals/approvals-main.webp 1000w, /blog/i/2000/octopus-approvals/approvals-main.webp 1261w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1261" height="555"></p>
</figure>
<h2 id="the-current-landscape">The current landscape</h2>
<p>Manual interventions were initially built to provide the ability to pause a deployment at crucial points to let a human review the state before progressing. We’ve seen customers build a makeshift approval system by stitching together a couple of manual intervention steps. This works for basic use cases but not particularly well at scale and has enforcement gaps.</p>
<p>The next step up is to use ITSM providers like SNOW and JSM. These are perfect for those in highly regulated industries with complex requirements. If you have strict redeployment processes or need to configure a subset of approvers, then that is still the right solution for you; we are not trying to replace these providers.</p>
<h2 id="why-weve-built-octopus-approvals">Why we’ve built Octopus Approvals</h2>
<p>Not everyone needs a full ITSM change management system, but many still want to know that a release has the buy-in of key stakeholders before it goes out the door. Octopus Approvals have been designed for customers occupying the middle ground, with more complex approval requirements than manual interventions can meet, but who can’t yet justify the cost of a full ITSM implementation.</p>
<p>Our aim with Octopus Approvals is to bridge this chasm as your compliance requirements grow and help you reach the next level. Octopus Approvals occur before deployment begins; approval is not a step that can be skipped, and it won’t take up a spot in your task queue until it has been approved. Within Approvals, you can explicitly set the parameters required to approve a deployment, including preventing the deployment creator from approving and specifying how many people must approve and from which team(s).</p>
<h2 id="octopus-approvals-walk-through">Octopus Approvals walk-through</h2>
<p>I want to set up an approval rule requiring at least two team members to approve a deployment before it goes to production. This applies across all projects in my team’s ownership, but only for production; I don’t want to slow down deployments to non-production environments.</p>
<p>Approvals are set at a space level; a single approval rule can span multiple projects. I’m going to use tags to determine which projects and environments are affected, enabling a more dynamic selection. Any new projects tagged as owned by my team will automatically have this rule applied.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-create.png" alt="Screenshot showing how to create an approval rule using tag sets" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-create.webp 600w, /blog/i/1000/octopus-approvals/approval-create.webp 1000w, /blog/i/2000/octopus-approvals/approval-create.webp 1298w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1298" height="956"></p>
</figure>
<p>When a project owned by Fire &#x26; Motion has a deployment created for a production environment, the task will wait for all required approvals. Change requests can be reviewed and approved from the deployment task and the change request tab within the approvals page. Once all required approvals have been received, the deployment will continue.</p>
<p>Each release requires only one approval; redeployments of the same release to the same environment do not require another approval. This ensures that rollbacks to approved versions are not blocked.</p>
<p>For scheduled deployments, an approval will be available as soon as the deployment is created, so you can schedule a deployment outside working hours but approve it before you clock off. This ensures that deployments stay within the approved change windows, but the checks can happen when it’s practical for the team.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-change-request.png" alt="Screenshot showing a change request awaiting review" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-change-request.webp 600w, /blog/i/1000/octopus-approvals/approval-change-request.webp 1000w, /blog/i/2000/octopus-approvals/approval-change-request.webp 1523w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1523" height="796"></p>
</figure>
<h2 id="rejected-change-requests">Rejected change requests</h2>
<p>If any approver rejects the change request, the deployment will not proceed and will be marked as failed. You can not revive a rejected change request; you’ll need to create a new deployment, which will create a new change request. You can use your normal process to identify failed deployments, or set up a Subscription to be notified of change request status.</p>
<h2 id="reviewing-and-approving-change-requests">Reviewing and approving change requests</h2>
<p>Change requests can be viewed on the Approvals page. From here, you can see who approved the deployment and when, along with deployment details. Approvals and changes to rules can also be tracked via the audit page; there are ‘Approval Rule’ and ‘Server Task Approval’ document types that the audit page can be filtered by.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-complete.png" alt="Screenshot showing a complete change request" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-complete.webp 600w, /blog/i/1000/octopus-approvals/approval-complete.webp 1000w, /blog/i/2000/octopus-approvals/approval-complete.webp 1936w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1936" height="1678"></p>
</figure>
<h2 id="notifications">Notifications</h2>
<p>The same event types used to filter the audit page can also be used to set up targeted notifications. Using our subscriptions feature, you can subscribe to notifications about rule changes to help ensure your team remains compliant. Notifications can be sent via email, Slack, Teams or webhook. Note the below screenshot is the first iteration of our Slack integration, we are currently working on providing a more actionable message.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-notification.png" alt="Screenshot showing a Slack notification following a change to an Approval Rule" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-notification.webp 300w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="300" height="163"></p>
</figure>
<p>We will be releasing a separate blog post detailing how you can use subscriptions and webhooks to receive more targeted notifications, so that users who need to approve the deployment are the ones who receive the notification.
<a href="https://octopus.com/software-delivered">Subscribe to our newsletter</a> to get this in your inbox.</p>
<h2 id="known-limitations">Known limitations</h2>
<ul>
<li>Any of the required approvers are counted towards the total. If you specified two teams within the approvers, any two approvals across those teams will suffice.</li>
<li>This cannot be used to automate your approval process.</li>
<li>Approvals only work for deployments; this functionality is not available for runbooks.</li>
<li>Approvals, unfortunately, cannot be used to finally get your mother-in-law’s approval.</li>
</ul>
<p>Octopus Approvals is now available as a Public Preview for cloud customers and in the 2026.3 server release.</p>
<p><a href="https://octopus.com/docs/approvals/octopus-approvals">Read more about getting started with Approvals here</a>.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Inside Platform Engineering with Kian Rogerdi</title>
      <link href="https://octopus.com/blog/inside-platform-engineering-kian-rogerdi" />
      <id>https://octopus.com/blog/inside-platform-engineering-kian-rogerdi</id>
      <published>2026-09-25</published>
      <updated>2026-09-25</updated>
      <summary>Kian Rogerdi joins Matt Allford to talk about connecting Platform Engineering to business value, why platform teams should be enablers rather than gatekeepers.</summary>
      <author>
        <name>Matthew Allford, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>If you’ve got the best engineers in the world building the greatest product nobody uses, what was the point? That’s a core question that was lingering in my mind in this episode of Inside Platform Engineering, with Kian Rogerdi. Kian has spent over 20 years in software, the last 10 of them in leadership, running cloud engineering, SRE, and Platform Engineering departments at companies ranging from startups to big enterprises.</p>
<p>What stood out to me most was how much of this conversation ended up being about mindset rather than tooling. In fact, it’s been a common theme in many conversations I’ve had on Inside Platform Engineering, and I’m seeing it as a crucial element in successful platform teams. We didn’t talk much about specific platforms or products. We talked about how easy it is for a platform team to lose sight of who they’re serving, and what that looks like when it goes wrong.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p>You can watch the episode with Kian below.</p>
<p><a href="https://www.youtube.com/watch?v=omCDrSlp-5E">Inside Platform Engineering with Kian Rogerdi</a></p>
<h2 id="business-value-is-the-point">Business value is the point</h2>
<p>Kian’s framing is simple, but as a technologist, can be easy to lose sight of. Everything a platform team does only matters if it’s actually helping someone else move faster and safer. He pointed out that plenty of teams still measure themselves on technical excellence alone, building the most secure, most powerful, most elegant system, without asking whether anyone’s using it or whether it’s solving a real problem. Product teams sorted this out years ago because they have a customer staring back at them. Platform teams don’t get that same forcing function, since their customers are internal, which is exactly why it’s harder and why it gets missed. That mindset needs to be a deliberate choice from day 1 when forming a platform team, backed by sensible success metrics to help platform teams showcase their value.</p>
<h2 id="treat-your-internal-teams-like-customers">Treat your internal teams like customers</h2>
<p>This idea of treating internal teams as real customers has come up in previous conversations, even even as far back as <a href="https://youtu.be/gKReR1YYMgU">Inside Platform Engineering episode 001</a> with Luke Philips. Kian leaned on Team Topologies, and it’s worth a read if you haven’t. His point was that platform teams are supposed to reduce cognitive load for stream-aligned teams, but that only works if you’re listening to those teams the way a product team listens to its customers. He talked about embedding platform engineers with the teams they serve, just to watch what actually happens, which templates get ignored, which pipelines get quietly replaced with something homegrown. I really love this take, and I’ve seen versions of this myself, where teams don’t abandon your tooling because they’re difficult, they abandon it because it doesn’t solve their problem.</p>
<p>It reminds me of when I was in a consultative role as a DevOps Engineer, and we were a little too on the reactive side where we were waiting for customers to come to us when they needed help. We decided to flip the model on its head, and asked to sit in with our customers’ developers on their daily standup. We weren’t in every standup, just one or two a week. Just doing this paved the way for a much better way of working <em>with</em> their teams. We could ask questions like “why are you doing it that way” or suggest “we can make that a lot easier”, and because we were in the room, they would ask for our opinion on the topics of discussion in that meeting. Dev and Ops coming together, who would have thought! I see this same pattern as crucial for platform teams, who need to spend time with the teams they’re trying to enable.</p>
<h2 id="enablers-not-gatekeepers">Enablers, not gatekeepers</h2>
<p>Speaking of enabling, Kian described a pattern he’s seen play out at companies he’s worked at, where a platform team that starts dictating instead of enabling, vetoing decisions, and effectively becomes a gate that other teams have to get through. He drew a clear line around genuine compliance requirements in regulated industries like fintech or healthcare, which are legitimately non-negotiable, but said everything beyond that is a red flag. He also noted this tends to get worse as companies scale, because politics creeps in and platform work becomes a source of power over other teams rather than a service to them. The fix, in his words, isn’t complicated; it’s remembering the purpose. It just takes deliberate effort to keep remembering it.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Inside Platform Engineering is a series of conversations with Matt Allford and a guest, bringing their own experience and perspective from the world of Platform Engineering.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw24Y-7jTcw09jbzsLw5uL9X">YouTube</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.9: The compliance ratchet</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e9" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e9</id>
      <published>2026-09-22</published>
      <updated>2026-09-22</updated>
      <summary>Find out why compliance is a one-way ratchet that only ever adds controls and never removes them.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In the last episode, we talked about AI efficiency and effectiveness. That meant thinking not just as a beneficiary user but also as an end user. In this episode, we bring the authority to bear when we discuss <a href="https://octopus.com/blog/the-compliance-ratchet">the compliance ratchet</a>. It turns out that there is an equal and opposite reaction in software, just like physics. If you speed up your software delivery velocity without the right practices in place, you encounter trip hazards that cause your organization to tighten controls.</p>
<p>The most dangerous part of this reaction is that compliance controls are added very easily, but are difficult or impossible to remove. That means you can trigger a one-way ratchet that permanently hampers your software delivery performance.</p>
<p>At the moment, it’s common for teams to use AI to increase the volume and pace of their software development. If this results in outages or high-profile bugs, compliance controls are slapped on the team. This often pushes the pace of change back to pre-AI levels, preventing any return on the AI investment.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p><a href="https://www.youtube.com/watch?v=bHlXI26jbug">Watch Continuous Delivery Office Hours Ep.9</a></p>
<h2 id="balance-the-ai-investment">Balance the AI investment</h2>
<p>One of the mistakes of introducing AI coding tools is expecting immediate gains. This puts pressure on teams to produce more changes and discourages them from introducing the pipelines and practices that will unlock the velocity and safety of sustainable high-performance software delivery.</p>
<p>On top of balancing the need for enabling practices, it’s also crucial to balance where you apply AI. If you only use AI to increase coding speed, the increased change volume will be absorbed by downstream bottlenecks. That means you need to consider where the investment has the greatest impact on the end-to-end flow of value, rather than speeding up individual stages and pushing more work into queues for review, approval, deployment, and feedback.</p>
<h2 id="unlock-the-ratchet">Unlock the ratchet</h2>
<p>The controls you have in place for software delivery were likely created for a different time. They accrue as a result of organizational trauma: a bad deployment during a peak period means a 3-month deployment freeze, or a bad dependency means every change needs a senior developer review. Even if the team has learned from those mistakes, the controls remain.</p>
<p>Now might be a good time to revisit your path from a developer’s laptop to production, documenting each control and deciding whether it’s still necessary, and whether there’s a better way to manage the controls you decide to keep, optimizing for both speed and stability. Each past decision made on the assumption that there was a trade-off between velocity and safety should be reconsidered to find a way to achieve both. Automated validation is often faster and more repeatable than manual validation, so you likely have a way to achieve both easily. Replacing manual stages with automation provides far better control than adding additional slow checks.</p>
<h2 id="direction-often-beats-speed">Direction often beats speed</h2>
<p>If you’ve been hanging around in software development for any length of time, you’d think all people cared about was speed. This is one of the great distractions of the past 3 decades. Teams adopted Agile for speed, they adopted DevOps for speed, and now they’re buying AI coding tools for speed. As described in <a href="https://octopus.com/blog/the-bullseye-model-for-product-velocity">The Bullseye Model</a>, the ultimate goal isn’t speed, but a combination of speed and direction. When you aren’t sure what users want, delivering software versions early and often helps you discover their needs and what works. Ultimately, though, you’re trying to hit the bullseye with perfect software, and that means direction is at least as important as speed, perhaps more so.</p>
<p>All of these insights point to a necessary shift in software delivery; one that is long overdue. For a long time, we’ve treated technical practices as optional, but with the velocity and scale of modern software, that’s no longer true. We’ll be talking more about this in the future.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Continuous Delivery Office Hours is a series of conversations about software delivery, with Tony Kelly, Bob Walker, and Steve Fenton.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw3CrxkUOAMmiy928lr5D4oh">YouTube</a>, <a href="https://podcasts.apple.com/us/podcast/continuous-delivery-office-hours/id1872101651">Apple Podcasts</a>, and <a href="https://pca.st/hwjaox59">Pocket Casts</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Let's not water down the terms Continuous Integration and Continuous Delivery</title>
      <link href="https://octopus.com/blog/watering-down-ci-cd" />
      <id>https://octopus.com/blog/watering-down-ci-cd</id>
      <published>2026-09-08</published>
      <updated>2026-09-08</updated>
      <summary>Find out why CI/CD aren't vague buzzwords. Here's what Continuous Integration and Continuous Delivery actually mean, and the common misconceptions worth correcting</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>One of the most perplexing discussions I’ve had in recent times was about CI/CD. I’ve earned my CI/CD scout badge at this stage in my career, so the confusion caught me off guard. This post briefly sets out what Continuous Integration (CI) and Continuous Delivery (CD) mean and addresses the most common misconceptions in the discussion.</p>
<p>My point isn’t just semantic. These terms carry decades of hard-won engineering practice, and when we let their meaning drift or decay, we lose crucial wisdom.</p>
<h2 id="continuous-integration">Continuous Integration</h2>
<p>Continuous Integration is well named compared to many concepts in our industry. The term “integration” refers to merging your changes into the main branch in version control, and the term “continuous” means you do it often. When Kent Beck popularized the term in <a href="https://www.oreilly.com/library/view/extreme-programming-explained/0201616416/">Extreme Programming Explained</a> (1999), he set an upper limit of a day. After 26 years of progress, you should expect to be doing it far more frequently than that.</p>
<p>You might understand the concept better if we look at examples that don’t fit the definition. If you have code on your machine that’s more than a day old, you’re not practicing Continuous Integration. If you have a branch that’s more than a day old, you’re not practicing Continuous Integration. It doesn’t matter how often you merge from main into your branch, because main is completely unaware of changes, like yours, that haven’t been merged yet. That means you have diverging software versions, and at some point, you have to make them all converge.</p>
<h2 id="continuous-delivery">Continuous Delivery</h2>
<p>Continuous Delivery is an approach focused on delivering all changes to users quickly, safely, and sustainably. You keep your code in a deployable state at all times and reject the idea of separate integration, testing, and hardening phases. There are specific technical practices involved here because you need a deployment pipeline that reduces risk and increases your confidence that the software version works.</p>
<p>At this stage in our industry’s history, almost everything involved in answering the question “Is this deployable?” should be automated as part of the deployment pipeline. A decade ago, perhaps, you could reasonably list a few stages that were economically unviable for automation. The scale and pace of modern software delivery mean this is rarely the case now.</p>
<p>The simple test is to ask, “Is the software deployable?” If you can’t answer this question every day, you’re not doing Continuous Delivery. In their book on <a href="https://www.oreilly.com/library/view/continuous-delivery-reliable/9780321670250/">Continuous Delivery</a> (2010), Dave Farley and Jez Humble detail the principles and practices that let you answer this question, and the research has increased our confidence that this is what good software delivery looks like.</p>
<h2 id="misconceptions">Misconceptions</h2>
<p>Let’s turn our attention to where the conversation left the trail.</p>
<h3 id="ci-doesnt-just-mean-automated-builds">CI doesn’t just mean automated builds</h3>
<p>One of the most common sources of confusion is that CI just means automated builds and tests. While it’s difficult to practice Continuous Integration without automation, the crucial property of Continuous Integration is that all changes are in a shared mainline, not on developer machines or in long-lived branches. Having more than 3 branches or keeping branches open for more than a day slows delivery throughput and makes the software less stable, as reinforced by <a href="https://dora.dev/research/">long-running research</a>.</p>
<p>There’s a human process at the heart of CI: each developer chooses to commit their changes frequently to main. Without people deciding to do this, there is no CI.</p>
<h3 id="ci-doesnt-allow-feature-branching">CI doesn’t allow feature branching</h3>
<p>There’s a misconception that you can do CI and choose a branching strategy, like feature branches. Any branching strategy that keeps a change out of main for more than a day cannot be called CI. You may still choose to use feature branches despite the drawbacks, but you have to stop calling it Continuous Integration, as you aren’t continuously integrating all changes.</p>
<h3 id="cd-doesnt-mean-continuous-deployment-though-it-can">CD doesn’t mean Continuous Deployment, though it can</h3>
<p>CD means “Continuous Delivery”. It means your software is deployable at all times, and you can test this daily by asking, “Can we deploy the software?” If you don’t have a way to answer the question confidently, you’re not doing Continuous Delivery. You may choose to deploy every validated software version automatically, which is Continuous Deployment, or you may need to be more tactical about when you deploy. Either is fine.</p>
<p>For both Continuous Delivery and Continuous Deployment, you need high levels of automation and sufficient risk coverage within that automation so you know you can deploy. Continuous Delivery should mean you can deploy at the press of a button, and Continuous Deployment removes the requirement to press the button.</p>
<h3 id="you-cant-do-cd-without-ci">You can’t do CD without CI</h3>
<p>Some folks were confident that you can do Continuous Delivery without doing Continuous Integration, but the literature on this is very clear. Farley and Humble explicitly include Continuous Integration as a required practice. There’s a whole chapter dedicated to it (p. 55 of Continuous Delivery) that adequately explains how it should work and why it’s not optional.</p>
<h3 id="cicd-isnt-vague-or-open-to-interpretation">CI/CD isn’t vague or open to interpretation</h3>
<p>Another claim that came up more than once was that CI/CD is vague, open to interpretation, or lacks the detail on “how” you should apply the approach. This does a disservice to those who conscientiously captured the approach, whether that’s Kent Beck describing Continuous Integration in <a href="https://www.oreilly.com/library/view/extreme-programming-explained/0201616416/"><em>Extreme Programming Explained</em></a> (1999) or Farley and Humble detailing both Continuous Integration and Continuous Delivery in their book <a href="https://www.oreilly.com/library/view/continuous-delivery-reliable/9780321670250/"><em>Continuous Delivery</em></a> (2010).</p>
<h2 id="resist-the-diffusion">Resist the diffusion</h2>
<p>There is a constant temptation in the tech industry to allow <a href="https://martinfowler.com/bliki/SemanticDiffusion.html">semantic diffusion</a> to erode our progress. When we package up a cartload of wisdom into a term, we have to resist the diffusion that follows when people oversimplify our term, remove crucial parts of the whole, or try to brand something with the label that pushes us backward.</p>
<p>We have a history of giving up on these battles, as we seem to have done with Agile and DevOps. These terms had an important meaning that represented a significant step forward for our industry, and losing our energy in their defense lets the perpetual “Late Majority” and “Laggards” (from <a href="https://geoffreyamoore.com/book/crossing-the-chasm/">Crossing the Chasm</a>) drag us all back into their old ways.</p>
<p>CI and CD deserve better.</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Runbook Webhooks</title>
      <link href="https://octopus.com/blog/octo-easy-mode-23-runbookwebhooks" />
      <id>https://octopus.com/blog/octo-easy-mode-23-runbookwebhooks</id>
      <published>2026-09-01</published>
      <updated>2026-09-01</updated>
      <summary>Learn how to create runbook webhook triggers</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Runbooks provide a way to automate ad-hoc and maintenance tasks, and <a href="https://octopus.com/docs/runbooks/webhook-runbook-trigger/">webhooks</a> allow you to trigger these runbooks from external systems. In this post, you’ll create a sample project that demonstrates how to set up runbook webhook triggers.</p>
<p><a href="https://samples.octopus.app/app#/Spaces-1213/projects/script-app-with-runbook-webhooks/operations/triggers">Live demo</a></p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>An <a href="https://octopus.com/start">Octopus Cloud</a> account. If you don’t have one, you can sign up for a free trial.</li>
<li>The Octopus AI Assistant Chrome extension. You can install it from the <a href="https://chromewebstore.google.com/detail/octopus-ai-assistant/agfpjjibnieiihjoehophlbamcifdfha">Chrome Web Store</a>.</li>
</ul>
<div class="hint"><p>The Octopus AI Assistant will work with an on-premises Octopus instance, but it requires more configuration. The
cloud-hosted version of Octopus doesn’t need extra configuration. This means the cloud-hosted version is the easiest way to get started.</p></div>
<h2 id="creating-the-project">Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a Script project called "23. Script App with Runbook Webhooks", and then:</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Add a runbook called "Runbook Webhook Trigger" with a script step that echoes "Runbook triggered by webhook".</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Create a webhook trigger for the runbook, using a webhook secret.</span></span></code></pre>
<p>The project <code>23. Script App with Runbook Webhooks</code> will be created, along with a runbook called <code>Runbook Webhook Trigger</code> that contains a script step to echo the message “Runbook triggered by webhook”. A webhook trigger will also be set up for the runbook, using a webhook secret.</p>
<p>For security reasons, the AI Assistant does not accept secrets as inputs, and the webhook secret is generated automatically. You must replace the secret with your own value:</p>
<p><img src="/blog/i/x/octo-easy-mode-23-runbookwebhooks/webhook-secret.png" alt="Webhook Secret" loading="lazy" srcset="/blog/i/600/octo-easy-mode-23-runbookwebhooks/webhook-secret.webp 600w, /blog/i/1000/octo-easy-mode-23-runbookwebhooks/webhook-secret.webp 1000w, /blog/i/2000/octo-easy-mode-23-runbookwebhooks/webhook-secret.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3456" height="1992"></p>
<div class="hint"><p>Even if you try to define a secret in the prompt, the AI Assistant will ignore it and generate a new random secret that is immediately forgotten and not displayed anywhere. You must replace the secret with your own value.</p><p>Webhooks can also use an Octopus API key for authentication.</p></div>
<p>Your webhook has a unique URL that you can use to trigger the runbook. This is displayed in the <code>Endpoint</code> section of the webhook trigger.</p>
<p>Use the following curl command to execute the runbook:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">curl</span><span style="color:#0000FF"> -X</span><span style="color:#A31515"> POST</span><span style="color:#0000FF"> -H</span><span style="color:#A31515"> "X-Octopus-Webhook-Secret: &#x3C;webhook-secret>"</span><span style="color:#A31515"> https://</span><span style="color:#000000">&#x3C;</span><span style="color:#A31515">your-octopus-ur</span><span style="color:#000000">l></span><span style="color:#A31515">/api/Spaces-</span><span style="color:#000000">&#x3C;</span><span style="color:#A31515">space-i</span><span style="color:#000000">d></span><span style="color:#A31515">/webhook/</span><span style="color:#000000">&#x3C;</span><span style="color:#A31515">webhook-i</span><span style="color:#000000">d></span></span></code></pre>
<h2 id="what-just-happened">What just happened?</h2>
<p>You created a sample project with:</p>
<ul>
<li>A runbook called <code>Runbook Webhook Trigger</code> that contains a script step to echo a message to the logs.</li>
<li>A webhook trigger for the runbook, using a webhook secret.</li>
</ul>]]></content>
    </entry>
    <entry>
      <title>Debunking the hotfix pipeline myth</title>
      <link href="https://octopus.com/blog/the-hotfix-myth" />
      <id>https://octopus.com/blog/the-hotfix-myth</id>
      <published>2026-08-27</published>
      <updated>2026-08-27</updated>
      <summary>Why it is more important to making the default pipelines as fast and as efficient as possible over hotfix pipelines.</summary>
      <author>
        <name>Bob Walker, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Hotfix pipelines are like an extended warranty; they make sense in principle, but in reality, they aren’t necessary.  And just like an extended warranty, you spend a lot of money (and time) building a hotfix pipeline that you should use only rarely.  This article will first walk through the fundamental flaws of hotfix pipelines.  Then it will explain why it is better to focus on improving the standard Production deployment pipeline.</p>
<h2 id="defining-hotfix-pipelines">Defining hotfix pipelines</h2>
<p>Hotfix pipelines are a “fast lane” to the normal Production deployment pipeline. Their purpose is to skip specific steps to get changes into Production as soon as possible.  Typically, they are:</p>
<ul>
<li>Production deployment pipeline: Development → Test → Staging → Production</li>
<li>Hotfix pipeline: Staging → Production</li>
</ul>
<p>They exist because of this scenario:</p>
<ol>
<li>A critical bug appears in Production hours or days after a deployment.</li>
<li>A rollback is impossible, as that will cause more problems than it solves.</li>
<li>There is work in flight in lower environments.</li>
<li>The hotfix pipeline skips the lower environments.</li>
</ol>
<p>The core idea behind hotfix pipelines is that they allow normal work to continue while providing a path to fix a critical bug as soon as possible.  Someone, either within the development team or the business, determines that a bug is important enough to fix and can skip use the hotfix pipeline.</p>
<h2 id="hotfixes-in-the-real-world">Hotfixes in the real world</h2>
<p>From experience, hotfix pipelines are created in response to a suboptimal Production deployment pipeline.  There is a step, or several steps, that either add little value or take so much time that everyone is fine skipping them when certain conditions are met.</p>
<p>I experienced a suboptimal process before joining Octopus Deploy.  Before Octopus Deploy, I was a software engineer for almost 15 years.  This was a deployment process at a company I worked for before we fully automated it (using Octopus Deploy, of course!).</p>
<ul>
<li>Four environments: Development, QA, Integration, and Production.</li>
<li>Shared database for the same application for all in-flight work.</li>
<li>The build server only monitored the main branch.</li>
<li>The build server automatically deployed to Development and QA.</li>
<li>Deploying to Integration and Production used different tooling and was a mix of manual and automated.</li>
</ul>
<p>As you can see, deploying to Integration and Production was completely different from Development and QA.  Some notable new steps include placing code build artifacts in “hot folders” to automatically deploy code, placing database delta scripts in designated network folders, and hand-typing instructions for the Web Admins and DBAs.</p>
<p>Deployments to Production took hours due to missing database and configuration changes, and occasional failures to update a server with the latest code.  Because deployments to Production took hours and were error-prone, the application I was responsible for deployed new functionality and most bug fixes to Production once a quarter.  Those deployments were always off-hours.</p>
<p>Integration was slightly better; we could deploy to that in the middle of the day, but it was still just as error-prone.  We didn’t want to deal with that headache.  Integration was often updated only days before a Production deployment.  As a result, Integration and Production were almost always running the same version.  This created a clear hotfix path for us.</p>
<p>Generally, but not always, hotfixes could be placed into one of four buckets.  In each case, we could run the proposed fix in Integration before Production.</p>
<ul>
<li>Fix the issue by changing a record in the database to “trick” the code into the right path.</li>
<li>Solve the problem by making a change to the database schema or a stored procedure.  Sometimes it was as simple as adding an index.</li>
<li>Make a configuration change, either in a web.config file or the operating system host.</li>
<li>If it required a code fix, we could configure the build server to create the build artifacts but not deploy to Development and QA.  We’d copy the artifacts to the “hot folders” for a deployment to Integration.</li>
</ul>
<p>The common workflow when a bug was reported was for the on-call engineer to triage it.  Once they isolated the root cause, they’d pair with a DBA or Web Admin until the issue was resolved.  While issues were resolved, it led to other problems.</p>
<ol>
<li>Database schema hotfix changes were rarely “copied down” to lower environments.  The delta in the database schema between Dev/QA and Integration/Production was significant.</li>
<li>Configuration changes sometimes didn’t make it into version control.  The next deployment to Integration and Production would often overwrite the hotfix, requiring another hotfix.</li>
<li>Because of the once-a-quarter releases, finding an appropriate point in time in version control that just had what was in Integration/Production took a lot of effort.</li>
</ol>
<h2 id="impact-of-automation-on-hotfix-pipelines">Impact of automation on hotfix pipelines</h2>
<p>Automating a terrible process doesn’t make it any less terrible.  The core problems must be addressed.  Our core problem was two different processes to deploy software from Development through to Production.  Those two processes were required because the build server was used for deployments and shared application database.  The build server lacked the appropriate RBAC and configuration controls to satisfy security and audit requirements.  The shared database encouraged the merging of unfinished code.</p>
<p>Using a combination of local database development, git, Redgate’s tooling, Octopus Deploy, and Azure DevOps (or Visual Studio Team Services as it was called back then), we rebuilt the entire deployment pipeline.</p>
<ul>
<li>All changes, code, and database are made in a branch.</li>
<li>Changes are merged to the main using a Pull Request.</li>
<li>Azure DevOps / VSTS builds the code and packages database changes.</li>
<li>Octopus + Redgate deploys those build artifacts to Development.</li>
<li>Automated Verification, then Octopus + Redgate, promotes those artifacts to QA.</li>
<li>QA verifies.</li>
<li>Promote those artifacts to Integration using Octopus + Redgate.</li>
<li>Final sign-off.</li>
<li>Promote those artifacts to Production using Octopus + Redgate.</li>
</ul>
<p>Rebuilding the deployment pipeline had numerous benefits.</p>
<ol>
<li>Consistent deployments: No more surprises when deploying to Integration and Production, as it was the same process for Development and QA</li>
<li>Eliminated post-deployment emergency fixes: Automation stopped the dumb mistakes that led to many hotfixes.</li>
<li>Reduced manual work: No one had to copy files to specific network folders and write up instructions.</li>
<li>Increased release cadence: Production deployments increased from once a quarter to once a week.</li>
<li>Smaller changeset: Instead of dozens or 100s of changes, each deployment had fewer than a dozen.  That meant there was a lower chance that something could go wrong.</li>
</ol>
<p>The most surprising thing to us was the more tolerant users.  When it took two or three months to get a bug fix into production, users would classify most bugs as high/must-fix because they couldn’t wait that long. With the new pipeline, unless the bug was critical, most users were willing to wait a day or two for a fix.  With smaller change sets, the number of critical bugs dropped significantly.</p>
<p>While we were deploying once a week, we still had major features that would take over a month to develop.  All that work would occur in a branch.  When we merged in those major changes, it would sometimes take a week or two to fully test it (our actual average was deploying every 10 days).  During that time, there was “no clear path to production” for hotfixes.</p>
<ul>
<li>For major features, we’d often do a few deployments to Integration as a final test with Production-like data.</li>
<li>Once major features were pushed to Integration, there was no easy way to roll back those changes.</li>
<li>We used a Service-Oriented Architecture (SOA) to achieve loose coupling, but there was still some coupling.  Whenever anyone rolled back Integration to match Production, other applications would break because they were expecting a specific version.</li>
</ul>
<p>Because we knew there was a chance of bugs after deploying a major feature to Production, we would implement a merge freeze, except for critical bugs, for a week after the Production deployment.</p>
<p>The merge freeze was the best solution we could come up with at the time.  We looked into creating a hotfix pipeline, but with deployments to Production occurring once a week (or so), we kept running into the same roadblocks:</p>
<ul>
<li>How do we qualify a bug to justify using the hotfix pipeline?</li>
<li>What if a pending change is in Integration?</li>
<li>How do we configure the build server to tell Octopus to push to Integration instead of Development?</li>
<li>How do we know which branch would be in Production when another bug occurred with a hotfix pipeline?</li>
</ul>
<p>The decision tree of if/then/elseif/else for when to use a hotfix pipeline became extremely confusing.  In addition, fewer critical bugs were released.  The work to create a hotfix pipeline for my team was de-prioritized.  Until I left, I never saw that becoming a priority.  Every once in a while, when we had a merge freeze, someone would bring it up.  But it was more of a passing comment.  Not a dictate to make a change.</p>
<h2 id="hotfixes-dont-fit-in-modern-software-delivery">Hotfixes don’t fit in modern software delivery</h2>
<p>The story from above took place over 10 years ago.  It involved deploying .NET applications to static environments hosted by Windows Servers.  There were many Continuous Delivery techniques we didn’t implement at the time.  In addition, many technologies and techniques have been introduced.</p>
<p>Below are the core principles every software delivery pipeline should follow.  See <a href="http://ContinuousDelivery.com">Continuous Delivery</a> and <a href="https://octopus.com/whitepapers/achieving-continuous-delivery-with-tpf">Achieving Continuous Delivery with TPF</a>, and <a href="https://trunkbaseddevelopment.com">Trunk-Based Development</a> for more details.</p>
<ul>
<li>The main branch must always be in a deployable state.</li>
<li>Create the build artifacts from the main branch once and promote them through the necessary environments to production.</li>
<li>How you deploy to Production should be exactly the same as how you deploy to Development, Testing/QA, and Staging/Integration.</li>
<li>Automate as much testing as possible, including unit testing, integration testing, and post-deployment smoke and soak tests.</li>
<li>Store environment configuration in version control and use that to keep all environments similar.  Compute resources and external access can differ by environment.</li>
<li>Separate deploying new code from releasing new functionality by using feature flags.</li>
</ul>
<p>With those core principles in mind, the delivery pipeline is the following:</p>
<ol>
<li>Make any changes in a short-lived branch.</li>
<li>Checking in changes to a branch creates a pre-release artifact and deploys to an ephemeral environment or a static Development environment.</li>
<li>Merge those changes into the main branch via a pull request.</li>
<li>The pull request should also be verified on an ephemeral environment or a static PR environment.</li>
<li>After merging into the main branch, create the release artifacts.</li>
<li>Promote those release artifacts through any static testing environments (Test, QA, Staging, Pre-Production, etc.) to Production.</li>
<li>Once a feature is ready, enable the new functionality for a subset of users in Production via feature flags.  Start with internal teams, then slowly add users until you have enabled it for all users.</li>
</ol>
<figure><p><img src="/blog/i/x/hotfix-myth/branching-diagram-with-ephemeral-environments.png" alt="Diagram demonstrating when ephemeral environments will be used in a trunk-based or GitHub Flow based branching strategy" loading="lazy" srcset="/blog/i/600/hotfix-myth/branching-diagram-with-ephemeral-environments.webp 600w, /blog/i/1000/hotfix-myth/branching-diagram-with-ephemeral-environments.webp 1000w, /blog/i/2000/hotfix-myth/branching-diagram-with-ephemeral-environments.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2219" height="1091"></p></figure>
<p>With the appropriate guardrails around the main Production deployment pipeline, a hotfix pipeline for just Staging → Production raises a lot of questions.</p>
<ul>
<li>Branching and Deploying
<ul>
<li>Will the hotfix branch use an ephemeral environment for testing before going to Staging?</li>
<li>How will the build server know to skip the Test environment and move to Staging → Production?</li>
<li>What will the version number be for the hotfix release?  If Production is 2026.8.1, does that mean the hotfix is 2026.8.1-Hotfix, 2026.8.1.1, or 2026.8.1.1-hotfix?</li>
<li>The main branch is supposed to represent production. How will the appropriate hotfix be communicated to the rest of the engineering team?</li>
<li>When will the hotfix changes merge into the main branch?  How much of a delta is there between what is in the main branch and production?  Can the fix even be merged into the main branch without serious modifications?</li>
</ul>
</li>
<li>Testing and Risk
<ul>
<li>What is preventing the main branch from being deployed to Production?</li>
<li>If there is new functionality that’ll likely have many edge cases and potentially show-stopping bugs, why wasn’t it behind a feature toggle?</li>
<li>Were there changes already in Staging that were overwritten by the hotfix?  Will that impact other teams or applications?</li>
<li>What steps and tests are being skipped in the Test environment?</li>
<li>How much time is really being saved by skipping the Test environment?</li>
<li>What if the hotfix requires a hotfix?  How long is it acceptable to block the normal pipeline from deploying to Staging → Production?</li>
<li>How often is the hotfix pipeline tested and verified?</li>
</ul>
</li>
</ul>
<h2 id="hotfix-pipelines-no-longer-make-sense">Hotfix pipelines no longer make sense</h2>
<p>All of the challenges listed above are solvable with enough time and money.  But to quote Ian Malcolm from Jurassic Park, “Your scientists were so preoccupied with whether they could, they didn’t stop to think if they should.”</p>
<p>To put it bluntly:</p>
<ul>
<li>Is it worth spending time to work through all those issues to create a hotfix pipeline?</li>
<li>How often do you need to push a hotfix?</li>
<li>If it is a regular occurrence, is the hotfix process masking a suboptimal process like the one I described earlier?</li>
</ul>
<p>The time spent creating a hotfix pipeline is better spent making the primary software delivery pipeline as efficient as possible.  A good goal is to take less than an hour from pull request acceptance to being ready to deploy to Production.  That includes builds, testing, linting, scanning, deploying to lower environments, and verification.</p>
<p>Achieving that requires addressing some hard problems:</p>
<ul>
<li>If the main branch is regularly in an undeployable state, what testing and verification should be moved earlier in the pipeline to ensure it is always in a deployable state?</li>
<li>Is the branching strategy Trunk Based Development or GitHub Flow (not to be confused with GitFlow)?  If not, why not?  Even the creator of GitFlow has said <a href="https://nvie.com/posts/a-successful-git-branching-model/">not to use it for most applications.</a></li>
<li>Showstopping bugs, ones that typically require a hotfix, are typically the result of new features and functionality.  How can <a href="https://openfeature.dev">Feature Flags</a> be introduced into the pipeline to separate deploying new code from releasing new features?</li>
<li>If manual review and approval processes are the primary bottleneck, which steps in that process are prime candidates for automation to speed up approvals?</li>
</ul>
<p>The primary advantage of focusing on the items above is that they have a net positive for any change.  New features, security patches, and bug fixes will be deployable faster.</p>
<p>**Disclaimer:*- The one hour is a goal to aim for.  It isn’t a hard rule.  One hour for some applications, like monoliths, is impossible.  That doesn’t mean you shouldn’t try.  Small improvements add up over time.  Improving a monolith’s pipeline from one day to two hours is a huge accomplishment.</p>
<h2 id="configuring-octopus-deploy">Configuring Octopus Deploy</h2>
<p>By this point, you might be asking yourself, how does this impact my configuration of Octopus Deploy?</p>
<p>Unfortunately, lifecycles (and channels) are among the most misconfigured constructs within Octopus Deploy.  To make onboarding easier, the default lifecycle is built using conventions.  If you were to create Development, Test, Staging, and Production environments, the default lifecycle automatically becomes:</p>
<ul>
<li>Development → Test → Staging → Production</li>
</ul>
<p>That default lifecycle encourages bad behavior, necessitating a hotfix lifecycle.</p>
<ul>
<li>It doesn’t represent how developers work in branches.  Because it includes Production, it has to represent the main branch.  But it also includes Development.  To get feedback, developers are forced to use their local machine or merge unfinished changes into main.</li>
<li>With unfinished changes in the main branch, it is currently undeployable.  It could remain there for days and sometimes weeks.</li>
<li>Enforcing SemVer versioning rules becomes nearly impossible.  For example, releases that are not ready for Production typically receive a pre-release tag because the lifecycle includes Development and Production; that rule cannot exist.</li>
<li>There isn’t a clear path to push a fix to Production.  To prevent that from happening, teams will implement merge freezes for a period of time after a major release (like I did at a previous job).  Or create a hotfix lifecycle that skips Development and Test.</li>
</ul>
<p>The root cause of that is having the static Development environment included in the same lifecycle as Production.  Development is for unfinished changes, Production is for finished changes.  A Production lifecycle must never include Development.</p>
<p>If a static Development environment is required, my recommended lifecycles are:</p>
<ul>
<li>Default: Development</li>
<li>Release: Test → Staging → Production</li>
</ul>
<figure><p><img src="/blog/i/x/hotfix-myth/recommended-octopus-lifecycles.png" alt="Screenshot of Octopus Deploy interface showing the recommended lifecycles of default and release." loading="lazy" srcset="/blog/i/600/hotfix-myth/recommended-octopus-lifecycles.webp 600w, /blog/i/1000/hotfix-myth/recommended-octopus-lifecycles.webp 1000w, /blog/i/2000/hotfix-myth/recommended-octopus-lifecycles.webp 1951w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1951" height="558"></p></figure>
<p>**Disclaimer:*- The release lifecycle should include all static testing environments required to reach Production.  You might only need Test → Production, or Staging → Production.  I included Test → Staging → Production because, as an industry, we have coalesced around four environments.</p>
<p>The subsequent Project Channels are:</p>
<ul>
<li>Default (uses the default lifecycle or an ephemeral environment): build artifacts require a pre-release tag and can only be created from non-main branches.</li>
<li>Release (uses release lifecycle): build artifacts cannot have a pre-release tag and can only come from the main branch.</li>
</ul>
<figure><p><img src="/blog/i/x/hotfix-myth/recommended-octopus-channels.png" alt="Screenshot of Octopus Deploy interface showing the recommended default and release channels for a specific project." loading="lazy" srcset="/blog/i/600/hotfix-myth/recommended-octopus-channels.webp 600w, /blog/i/1000/hotfix-myth/recommended-octopus-channels.webp 1000w, /blog/i/2000/hotfix-myth/recommended-octopus-channels.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2033" height="1055"></p></figure>
<p>GitHub Actions (or really any build server) doesn’t make dynamically selecting channels based on branches any easier.  They require using a hard-to-decipher if/then/else command in the build definition.  For example, ${{ github.ref == ‘refs/heads/main’ &#x26;&#x26; vars.OCTOPUS_RELEASE_CHANNEL || vars.OCTOPUS_DEFAULT_CHANNEL }}.</p>
<h3 id="running-multiple-versions-in-production">Running multiple versions in Production</h3>
<p>Occasionally, REST APIs and other backend services must run multiple versions in Production for backward compatibility.  For the recommendations below, my example application has three versions: v1.x, v2.x, and v3.x.</p>
<ul>
<li>The main branch represents the latest version (v3.x)</li>
<li>Separate branches for each version (v1.x and v2.x)</li>
<li>Each version branch is treated like a “trunk”
<ul>
<li>Changes are made in short-lived branches that were branched off the version branch.</li>
<li>Merging into those version branches requires a pull request.</li>
</ul>
</li>
</ul>
<p>Within Octopus, you’ll only need one lifecycle:</p>
<ul>
<li>Release: Test → Staging → Production</li>
</ul>
<p>But the Project will have four Channels:</p>
<ul>
<li>Default
<ul>
<li>Uses an ephemeral environment</li>
<li>Build artifacts require a pre-release tag and can only come from non-version or main branches.</li>
</ul>
</li>
<li>vCurrent
<ul>
<li>Uses release lifecycle</li>
<li>Build artifacts cannot have a pre-release tag</li>
<li>Build artifacts must come from the main branch</li>
<li>Build artifacts version must be &#x3C;= 3.x</li>
</ul>
</li>
<li>V2
<ul>
<li>Uses release lifecycle</li>
<li>Build artifacts cannot have a pre-release tag</li>
<li>Build artifacts must come from the v2 branch</li>
<li>Build artifacts version must be between 2 and 2.999999</li>
</ul>
</li>
<li>V1
<ul>
<li>Uses release lifecycle</li>
<li>Build artifacts cannot have a pre-release tag</li>
<li>Build artifacts must come from the v1 branch</li>
<li>Build artifacts version must be between 1 and 1.999999</li>
</ul>
</li>
</ul>
<p>Ephemeral environments make this significantly easier, as you can spin up a sandbox for a change for any version and verify it before merging into the appropriate branch.  If you cannot use ephemeral environments, I’d recommend setting up a couple of static development environments and configuring a lifecycle that lets you deploy to any of them.  The downside is that a person must determine which static development environment to use.</p>
<h2 id="conclusion">Conclusion</h2>
<p>A Production incident is not the time to improvise a deployment pipeline.  Steps shouldn’t be skipped to “go faster.”  All too often, a “simple change” that isn’t properly vetted causes a bigger issue.  But that is essentially what a hotfix pipeline is designed to do.  It skips important steps in the normal Production deployment pipeline to save time.  To resolve a Production incident, you want a well-tested and well-used pipeline, so you know you aren’t introducing even more risk.  The time required to create and improve a hotfix pipeline is better spent improving the Production deployment pipeline.  Once the Production deployment pipeline takes less than an hour to be ready for deployment to Production, the need for a hotfix pipeline will be all but eliminated.</p>]]></content>
    </entry>
    <entry>
      <title>Inside Platform Engineering with Nigel Douglas</title>
      <link href="https://octopus.com/blog/inside-platform-engineering-nigel-douglas" />
      <id>https://octopus.com/blog/inside-platform-engineering-nigel-douglas</id>
      <published>2026-08-21</published>
      <updated>2026-08-21</updated>
      <summary>A conversation with Nigel Douglas on dependencies, supply chain attacks, and why most teams can't list everything holding their software together.</summary>
      <author>
        <name>Matthew Allford, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Most teams can’t easily list every dependency holding their software together. That is the gap I wanted to explore with Nigel Douglas, Head of Developer Relations at Cloudsmith, who spends his days helping developers understand and avoid supply chain compromises.</p>
<p>I’ll admit going in that this wasn’t a topic I’m fully across. I know enough to be dangerous, as I said to Nigel at the start, but I came away from this conversation with a much clearer picture of just how much of what we run in production came from outside our own codebase, and how little visibility most teams have into it.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p>You can watch the episode with Nigel below.</p>
<p><a href="https://www.youtube.com/watch?v=56zpHcbdQQQ">Inside Platform Engineering with Nigel Douglas</a></p>
<h2 id="you-dont-know-what-you-dont-know">You don’t know what you don’t know</h2>
<p>Nigel’s framing early on was great. Every library, every scanning tool, every base image you pull in is something someone else built, and each one is a new door into your environment. He used Falco as an example, a tool he uses for detection, and pointed out that the moment you adopt it, you’ve inherited whatever risk lives inside it. Multiply that by every dependency, and every dependency’s own dependencies, and you get what he called transitive dependencies stacking up into a problem nobody can fully see. As he put it, you don’t know what you don’t know. The real challenge is knowing what’s in your software stack across your organization, not just whether it’s secure.</p>
<h2 id="attackers-have-moved-from-ransomware-to-poisoning-the-well">Attackers have moved from ransomware to poisoning the well</h2>
<p>One of the most interesting parts of the conversation was Nigel’s read on why supply chain attacks have ramped up in the last couple of years, and his theory is refreshingly practical. Nation-state actors and organized groups are rational actors chasing the best return on effort. Ransomware against a single company might not even get paid. Compromising one maintainer account on a package downloaded a hundred million times a month is a far more lucrative outcome for the same amount of work. These groups didn’t suddenly discover supply chains, but instead realized the return on effort favors them there, given the scale.</p>
<h2 id="sboms-are-only-useful-if-you-keep-them">SBOMs are only useful if you keep them</h2>
<p>We spent a good chunk of the episode on software bills of materials (SBOM), and Nigel was honest that adoption is still low despite SBOMs being, in his words, a fantastic technology. Generating one isn’t the hard part, but keeping it useful is, since an SBOM is stale within hours as dependencies and their own transitive dependencies keep shifting underneath you.</p>
<p>His point about log4j resonated with me as I too lived through it, knowing the library was in use somewhere without knowing exactly where, then facing an uphill audit across every application to find the affected versions. If you’d been capturing SBOMs at every build and storing them somewhere like Dependency-Track, that same exercise becomes minutes of querying instead of hours of manual digging. Nigel compared this with insurance, where nobody cares about it until the moment something goes wrong, and that’s exactly the moment you need the historical record already sitting there.</p>
<h2 id="platform-teams-are-well-positioned-to-own">Platform teams are well positioned to own</h2>
<p>What I liked most about Nigel’s take is that he doesn’t see supply chain security as a bolt-on security team responsibility, he sees it as squarely a platform engineering job. A CISO can set the policy, but they’re not the ones building the golden paths developers actually use. Platform teams are already the ones deciding what goes into a build process, so they’re well placed to bake in tools like OSV scanning at the edge, or to route developers through a curated registry rather than pulling blindly from the public internet. His broader point was that the open tooling to do most of this already exists and is free, so the barrier really is accountability and ownership rather than a lack of technology.</p>
<h2 id="where-this-is-heading">Where this is heading</h2>
<p>Nigel didn’t sugarcoat the trajectory. AI is accelerating both vulnerability discovery and attackers’ ability to write and plant bad code, something he called, half jokingly, the “vulnpocalypse.” The volume of threats isn’t going to shrink, but he was equally clear that the defensive side has kept pace. The open source tooling for scanning, tracking and hardening what you run has matured fast, and platform teams have little excuse not to have a handle on this today.</p>
<p>AI’s impact on supply chain security is a topic I think I’ll get Nigel back for, so we can hyper-focus just on that one aspect of software development, and dig into both the positive and negative impacts.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Inside Platform Engineering is a series of conversations with Matt Allford and a guest, bringing their own experience and perspective from the world of Platform Engineering.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw24Y-7jTcw09jbzsLw5uL9X">YouTube</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Building an AI-Powered Incident Bot with Octopus Deploy</title>
      <link href="https://octopus.com/blog/ai-powered-incident-bot" />
      <id>https://octopus.com/blog/ai-powered-incident-bot</id>
      <published>2026-08-20</published>
      <updated>2026-08-20</updated>
      <summary>How I Built an AI-Assisted Kubernetes Incident Bot with Octopus Deploy.</summary>
      <author>
        <name>Patroklos Papapetrou, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Lately, I caught myself doing the same sequence of actions several times: Prometheus fires an alert (let’s say about a pod being in <code>CrashLoopBackOff</code>),
I search in the deployment/pod logs, realize that the service has run out of memory, then I open Octopus Deploy,
find the right project, then select the environment, bump the resource limit, and finally wait for the rollout to finish and check that the alert has cleared.</p>
<p>The actual fix looks (and it is) pretty straightforward. Most of my time was actually spent context-switching between terminal windows, Kubernetes configs, and Octopus UI tabs.
Most routine on-call alerts aren’t complex engineering problems. They often involve repeating the same remediation steps from an existing playbook.
We usually know what needs to happen, but we end up viewing and manually copying data between monitoring tools, logs, and deployment systems.</p>
<p>This is what made me build a PoC incident bot, which I call Octopus Healer. It’s a service that listens for Kubernetes alerts,
passes the pod context to an AI model to suggest a remediation, and maps the output to a predefined Octopus Deploy runbook.
Nothing reaches production until an operator reviews and approves the proposed runbook in Slack.</p>
<h2 id="why-octopus-deploy-is-the-right-execution-layer">Why Octopus Deploy is the right execution layer</h2>
<p>The first design decision was how to handle execution once my preferred AI model suggests a fix.
My initial approach while playing with the AI model was to have it generate kubectl commands—or, even better, execute them without manual intervention.</p>
<p>In a real production environment, though, running raw shell commands directly can quickly make things even worse. It can bypass existing approval flows, use overly broad permissions, and target the wrong environment. Instead of playing with fire, I decided to route all execution through Octopus Deploy.</p>
<p>The bot still handles the initial analysis: receiving the alert, collecting the relevant context, and sending the right prompt to the AI model. Octopus then acts as the execution engine, using the permissions, environments, and approval workflows that are already in place.
For teams like ours that follow GitOps principles and keep deployment configuration as code, this approach also provides a clean audit trail. Runbook creation, environment selection, approvals, and execution history all remain traceable alongside the rest of the deployment changes.</p>
<h2 id="the-full-pipeline">The full pipeline</h2>
<p>Here’s how everything connects, from Prometheus alert to Slack notification:</p>
<figure><p><img src="/blog/i/x/ai-powered-incident-bot/pipeline.png" alt="Full pipeline: Prometheus alert to Octopus runbook execution" srcset="/blog/i/600/ai-powered-incident-bot/pipeline.webp 600w, /blog/i/1000/ai-powered-incident-bot/pipeline.webp 1000w, /blog/i/2000/ai-powered-incident-bot/pipeline.webp 1381w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1381" height="678"></p></figure>
<p>The flow has three phases:</p>
<ul>
<li>Prometheus fires an alert; Alertmanager sends a webhook to the bot, which then fetches the affected pod’s logs and any other useful live metrics from the Kubernetes metrics-server (CPU/memory for a <code>CrashLoopBackOff</code>).</li>
<li>Once the bot has everything it needs, it sends that context to my favorite AI model, which returns a structured JSON analysis with a remediation type, confidence level, blast-radius classification, and the variable values needed to execute the fix.</li>
<li>The bot posts a Slack notification including the root cause and the available action type. The operator reviews the suggested fix, selects the environment, and approves the creation and execution of the runbook in Octopus Deploy. The bot then posts the result back to Slack.</li>
</ul>
<p>The whole tool is a single stateless service — no database, no message queue. Approvals live in an in-memory store with a 30-minute TTL.
If the operator doesn’t respond within that window, the approval expires, and the on-call engineer handles it manually.</p>
<h2 id="giving-an-ai-model-the-right-context">Giving an AI model the right context</h2>
<p>I initially sent far too much context to the model. Most of it was unnecessary, so I reduced the payload to the alert metadata, recent logs, resource configuration, and current resource metrics.
The goal was to provide enough information for a useful diagnosis without allowing large log payloads to dominate the prompt and increase the cost.</p>
<p>For each analysis, the bot collects a small but sufficient set of data to share with the AI model:</p>
<ol>
<li>Basic alert information, including the alert name, namespace, pod, and container.</li>
<li>For the PoC, I limited the payload to the most recent 4 KB. This worked well for the failure cases I tested and prevented large log payloads from dominating the prompt.</li>
<li>Depending on the alert and supported remediation type, the bot may collect additional information. For a pod in <code>CrashLoopBackOff</code>, this includes the configured CPU and memory requests and limits. When available, it also retrieves the pod’s current resource usage through the Kubernetes Metrics API.</li>
</ol>
<p>The response also needs to be predictable so the service can process it programmatically. The prompt asks the model to return the response in valid JSON only, without an introduction or a Markdown code block, and to use the predefined schema below.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#0451A5">  "root_cause"</span><span style="color:#000000">: </span><span style="color:#A31515">"Clear explanation of the problem"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "confidence"</span><span style="color:#000000">: </span><span style="color:#A31515">"HIGH|MEDIUM|LOW"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "remediation_type"</span><span style="color:#000000">: </span><span style="color:#A31515">"one of the above types"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "runbook_params"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">    "namespace"</span><span style="color:#000000">: </span><span style="color:#A31515">"{{.Namespace}}"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "deployment"</span><span style="color:#000000">: </span><span style="color:#A31515">"deployment name"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "key"</span><span style="color:#000000">: </span><span style="color:#A31515">"env var name — config_update only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "value"</span><span style="color:#000000">: </span><span style="color:#A31515">"new env var value — config_update only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "type"</span><span style="color:#000000">: </span><span style="color:#A31515">"env or secret — config_update only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "container"</span><span style="color:#000000">: </span><span style="color:#A31515">"container name — image_fix only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "registry"</span><span style="color:#000000">: </span><span style="color:#A31515">"registry URL — image_fix only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "image"</span><span style="color:#000000">: </span><span style="color:#A31515">"image name — image_fix only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "tag"</span><span style="color:#000000">: </span><span style="color:#A31515">"image tag — image_fix only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "cpu"</span><span style="color:#000000">: </span><span style="color:#A31515">"recommended CPU request e.g. 500m — resource_increase only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "memory"</span><span style="color:#000000">: </span><span style="color:#A31515">"recommended memory request e.g. 512Mi — resource_increase only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "cpu_limit"</span><span style="color:#000000">: </span><span style="color:#A31515">"recommended CPU limit e.g. 1000m — resource_increase only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "memory_limit"</span><span style="color:#000000">: </span><span style="color:#A31515">"recommended memory limit e.g. 1Gi — resource_increase only"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "target_revision"</span><span style="color:#000000">: </span><span style="color:#A31515">"revision number, 0 for previous — deployment_rollback only"</span></span>
<span class="line"><span style="color:#000000">  },</span></span>
<span class="line"><span style="color:#0451A5">  "manual_steps"</span><span style="color:#000000">: [</span><span style="color:#A31515">"any manual verification steps needed"</span><span style="color:#000000">],</span></span>
<span class="line"><span style="color:#0451A5">  "suggested_blast_radius"</span><span style="color:#000000">: </span><span style="color:#A31515">"single_pod|single_deployment|multiple_deployments|cluster"</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<p>The five remediation types the model can choose from and the suggested fix are shown below:</p>
<div class="table-wrap">




























<table><thead><tr><th>Type</th><th>What it does</th></tr></thead><tbody><tr><td><code>pod_restart</code></td><td>Rolling restart of the affected deployment</td></tr><tr><td><code>resource_increase</code></td><td>Scale CPU/memory — For the PoC, the bot uses a simple heuristic based on the currently available resource metrics: 1.5× usage for requests and 2× for limits</td></tr><tr><td><code>config_update</code></td><td>Patch a misconfigured environment variable or config map entry</td></tr><tr><td><code>image_fix</code></td><td>Roll forward to a corrected image tag</td></tr><tr><td><code>deployment_rollback</code></td><td>Roll back to the previous revision with a target-revision override</td></tr></tbody></table></div>
<p>For the proof of concept, I’ve hardcoded the remediation types and suggested fixes to make the development easier.
A production version could support a larger catalog of reviewed remediation templates. The model would still select from an allowlisted set rather than generating arbitrary execution logic.</p>
<h2 id="turning-the-models-analysis-into-a-runbook">Turning the model’s analysis into a runbook</h2>
<p>After validating the model’s JSON response, the bot maps the selected remediation type to an Octopus runbook template.
The implementation in this proof of concept is limited but clean: each supported remediation type maps to a predefined template whose script bodies use <code>$(variable)</code> placeholders that are filled with values from two sources — the alert itself (namespace, deployment name) and AI’s model <code>runbook_params</code>.
In the example below, the PoC supports only one Octopus runbook step type: kubernetes-script. Future versions could support additional step types provided by Octopus Deploy.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="go"><code><span class="line"><span style="color:#0000FF">func</span><span style="color:#795E26"> resourceIncreaseTemplate</span><span style="color:#000000">() *</span><span style="color:#267F99">RemediationTemplate</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#AF00DB">    return</span><span style="color:#000000"> &#x26;</span><span style="color:#267F99">RemediationTemplate</span><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#001080">        Steps</span><span style="color:#000000">: []</span><span style="color:#267F99">RunbookStep</span><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#000000">            {</span></span>
<span class="line"><span style="color:#001080">                Name</span><span style="color:#000000">:     </span><span style="color:#A31515">"Update Resource Limits"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#001080">                StepType</span><span style="color:#000000">: </span><span style="color:#A31515">"kubernetes-script"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#001080">                Properties</span><span style="color:#000000">: </span><span style="color:#0000FF">map</span><span style="color:#000000">[</span><span style="color:#267F99">string</span><span style="color:#000000">]</span><span style="color:#267F99">string</span><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#A31515">                    "scriptBody"</span><span style="color:#000000">: </span><span style="color:#A31515">"kubectl set resources deployment/$(deployment) -n $(namespace)"</span><span style="color:#000000"> +</span></span>
<span class="line"><span style="color:#A31515">                        " --requests=cpu=$(cpu),memory=$(memory)"</span><span style="color:#000000"> +</span></span>
<span class="line"><span style="color:#A31515">                        " --limits=cpu=$(cpu_limit),memory=$(memory_limit)"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#000000">                },</span></span>
<span class="line"><span style="color:#000000">            },</span></span>
<span class="line"><span style="color:#000000">            {</span></span>
<span class="line"><span style="color:#001080">                Name</span><span style="color:#000000">:     </span><span style="color:#A31515">"Trigger Rollout"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#001080">                StepType</span><span style="color:#000000">: </span><span style="color:#A31515">"kubernetes-script"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#001080">                Properties</span><span style="color:#000000">: </span><span style="color:#0000FF">map</span><span style="color:#000000">[</span><span style="color:#267F99">string</span><span style="color:#000000">]</span><span style="color:#267F99">string</span><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#A31515">                    "scriptBody"</span><span style="color:#000000">: </span><span style="color:#A31515">"kubectl rollout restart deployment/$(deployment) -n $(namespace)"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#000000">                },</span></span>
<span class="line"><span style="color:#000000">            },</span></span>
<span class="line"><span style="color:#000000">            {</span></span>
<span class="line"><span style="color:#001080">                Name</span><span style="color:#000000">:     </span><span style="color:#A31515">"Wait for Rollout"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#001080">                StepType</span><span style="color:#000000">: </span><span style="color:#A31515">"kubernetes-script"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#001080">                Properties</span><span style="color:#000000">: </span><span style="color:#0000FF">map</span><span style="color:#000000">[</span><span style="color:#267F99">string</span><span style="color:#000000">]</span><span style="color:#267F99">string</span><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#A31515">                    "scriptBody"</span><span style="color:#000000">: </span><span style="color:#A31515">"kubectl rollout status deployment/$(deployment) -n $(namespace) --timeout=5m"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#000000">                },</span></span>
<span class="line"><span style="color:#000000">            },</span></span>
<span class="line"><span style="color:#000000">        },</span></span>
<span class="line"><span style="color:#000000">    }</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<p>The <code>$(deployment)</code> and <code>$(namespace)</code> placeholders come from the alert. The resource values — <code>$(cpu)</code>, <code>$(memory)</code>, <code>$(cpu_limit)</code>, <code>$(memory_limit)</code> — come straight from the AI model’s response <code>runbook_params</code>.
The runbook generator then merges both sources and applies the model’s values to the runbook template, returning a ready-to-use runbook.
The last part is to talk to the Octopus API to create the live runbook, publish a snapshot and finally execute it in the environment selected by the operator.
Each runbook gets a unique, generated name that includes the remediation type, the incident resource, and a timestamp suffix (<code>resource_increase-api-pod-1722687423</code>), so every incident is traceable by type, workload, and time.</p>
<p>A couple of integration details that are worth highlighting are:</p>
<ol>
<li>Octopus Deploy supports two types of projects, standard and git-backed. This introduces some minor differences at the API level, but the bot handles both project types transparently.</li>
<li>The alert coming from Prometheus and the Octopus Deploy app know nothing about each other, so we need to create a link between the Kubernetes workload and its Octopus project. This can be easily done by adding an annotation on the Deployment as shown below:</li>
</ol>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#800000">metadata</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">  annotations</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">    octopus.com/project-id</span><span style="color:#000000">: </span><span style="color:#0000FF">Projects-42</span></span></code></pre>
<p>If the annotation is absent, the bot asks the operator to pick a project in Slack rather than failing silently.</p>
<figure><p><img src="/blog/i/x/ai-powered-incident-bot/slack-alert.png" alt="Slack bot asking the operator to pick a project when annotation is absent" srcset="/blog/i/600/ai-powered-incident-bot/slack-alert.webp 600w, /blog/i/1000/ai-powered-incident-bot/slack-alert.webp 678w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="678" height="608"></p></figure>
<p>In production, I would either require the annotation or restrict the Slack picker to an allowlisted set of projects.
These defensive fallbacks in the flow can be really helpful sometimes, but I wouldn’t rely on them for large-scale systems.</p>
<h2 id="fully-automated-or-operator-driven">Fully automated or operator-driven?</h2>
<p>While building the bot, I kept wondering how far I could take the automation. For local testing, I added an <code>AUTO_APPROVE=true</code> option that skips the Slack interaction and executes the generated runbook directly.</p>
<p>I would not enable that option in production based only on the confidence value returned by the AI model. A model reporting <code>HIGH</code> confidence does not guarantee that its diagnosis is correct or that the proposed action is safe.</p>
<p>For now, the production-oriented workflow keeps the operator involved at three points:</p>
<ol>
<li>The operator selects the target Octopus environment.</li>
<li>The bot generates the runbook and posts a preview of its steps in Slack.</li>
<li>The operator reviews the proposed actions and either approves or rejects the execution.</li>
</ol>
<p>Choosing the environment is a separate step because the bot cannot always determine the intended target from the Prometheus alert alone. After the environment is selected, the operator sees the actual runbook steps before anything is executed. This makes the approval more meaningful than simply asking someone to approve a short AI-generated description.</p>
<p>The blast radius is also shown in the approval flow. Instead of relying only on the model to classify it, the bot can derive most of the scope from the selected remediation template and its target. Restarting a single deployment, for example, is clearly different from applying a change across multiple workloads or at the cluster level.</p>
<p>A future version could allow some remediations to run automatically, but only when they pass a deterministic policy. That policy could require:</p>
<ul>
<li>an allowlisted remediation type;</li>
<li>a valid annotation linking the workload to a known Octopus project;</li>
<li>an environment that can be derived without operator input;</li>
<li>a limited blast radius;</li>
<li>validated parameters within predefined bounds;</li>
<li>and a successful dry run or policy check.</li>
</ul>
<p>The model’s confidence could still be included as an additional signal, but it should not be the control that authorizes execution.</p>
<p>There is another limitation to the current analysis. The model sees the alert, the pod configuration, recent logs, and current resource metrics, but it does not know everything that happened before the incident.</p>
<p>For example, a pod may start crashing immediately after a configuration change. Based only on the current symptoms, increasing its memory limit might appear reasonable. In reality, the correct action could be to roll back the most recent deployment. The suggestion may appear valid in the context provided to the model, yet be wrong because the important historical context is missing.</p>
<p>This is one of the areas I want to improve next. Adding recent Octopus deployments, configuration changes, image updates, and previous revisions to the diagnostic context would help the model distinguish between a resource problem and an incident caused by a recent change.</p>
<p>Until that context and the deterministic safety checks are in place, keeping an operator in the loop is not just an approval mechanism. It is part of the incident diagnosis.</p>
<figure><p><img src="/blog/i/x/ai-powered-incident-bot/slack-update-result.png" alt="Slack bot telling the operator about the Runbook execution outcome" srcset="/blog/i/600/ai-powered-incident-bot/slack-update-result.webp 479w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="479" height="292"></p></figure>
<p>After execution begins, the bot continues posting status updates in Slack until the runbook succeeds or fails</p>
<h2 id="what-i-learned--and-whats-next">What I learned — and what’s next</h2>
<p>Wiring an LLM into an automated deployment pipeline highlighted a few messy edge cases early on:</p>
<p>In the first version, I let the model generate the full <code>kubectl</code> command. That proved unreliable because in some cases, it returned <code>kubectl</code> flags that did not exist.
In other cases, it added Markdown or explanatory text even though the prompt requested only the command.</p>
<p>So I decided to change the design so the model no longer generates executable commands. It now selects one of the supported remediation types and provides only the required parameter values in a predefined JSON format.
The bot validates that response and uses those values to fill an existing runbook template.
This keeps the model involved in the diagnosis without allowing it to decide exactly which command will run.</p>
<p>Config as Code required more special handling than I expected. Config as Code required more special handling than I expected.
Supporting Git-backed projects meant maintaining separate code paths for many API operations. Compound runbook process IDs and Git-reference URL encoding were particularly tedious to debug. The additional complexity is worthwhile for the Git audit trail, but it increased the integration surface considerably.</p>
<p>The in-memory approval store is suitable only for the current PoC. Active approvals currently live in a Go map with a 30-minute TTL. This avoids adding an external dependency in a single-instance deployment, but restarting the pod during an incident removes all pending approvals. This is intentional technical debt for now. A production, highly available version would need shared storage such as Redis or PostgreSQL.</p>
<h3 id="whats-next">What’s next</h3>
<p>My immediate priority is finishing HMAC signature validation for incoming Slack webhooks. The signing secret is already parsed, but the request validation itself is not yet implemented, so the current PoC should not be exposed as a production Slack endpoint. After that, I plan to create a Helm chart, add support for more alert types, and include recent deployment history in the diagnostic context.
The main lesson from the PoC is that the model should help interpret the incident, not control execution. Keeping the remediation logic in reviewed Octopus runbooks makes the system easier to audit, validate, and operate safely.</p>]]></content>
    </entry>
    <entry>
      <title>Sandboxing local AI Agents</title>
      <link href="https://octopus.com/blog/local-ai-agent-sandboxes" />
      <id>https://octopus.com/blog/local-ai-agent-sandboxes</id>
      <published>2026-08-17</published>
      <updated>2026-08-17</updated>
      <summary>Learn how to approach security and sandboxing local AI agents</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In a <a href="/blog/ai-agent-sandboxes">previous post</a>, I discussed whether sandboxes were necessary for shared AI agents deployed in a corporate environment and concluded that, so long as the tools the agents use are secure, sandboxes are unnecessary.</p>
<p>However, local agents are a different story. Local agents are deployed on a developer’s machine and run arbitrary prompts, potentially with full access to the local environment. This makes it easy to accidentally or maliciously delete files, exfiltrate secrets, or otherwise compromise the local environment or any remote environment the local agent has access to.</p>
<p>In this post, I’ll discuss an approach to local sandboxes that contain the local agent while still providing much of the convenience when working in an IDE.</p>
<p>You can find the final Vagrantfile from <a href="https://github.com/OctopusSolutionsEngineering/AIVagrantSandbox">GitHub</a>.</p>
<div class="hint"><p><a href="/blog/octo-easy-mode-17-claude">Easy Mode - Claude Agent</a> provides a practical example incorporating Claude in deployments you can apply to your own Octopus instance:</p><ol>
<li>Install the <a href="https://chromewebstore.google.com/detail/octopus-ai-assistant/agfpjjibnieiihjoehophlbamcifdfha">Chrome extension</a></li>
<li>Run the prompt <code>Create a Claude project called "Categorize Changes"</code></li>
</ol></div>
<h2 id="in-brief">In brief</h2>
<ul>
<li>Describe the security risks of running local AI agents with full access to the local environment.</li>
<li>Present a VM sandbox built with Vagrant to restrict the local AI agent’s access to the local environment.</li>
<li>Discuss the trade-offs between security and convenience when running local AI agents in a sandbox.</li>
</ul>
<h2 id="why-local-sandboxes-are-necessary">Why local sandboxes are necessary</h2>
<p>If you have used any coding agents, you will be familiar with the confirmation prompts that are presented when the agent makes potentially destructive changes or may access sensitive information. While AI agents are getting better at presenting only those prompts that genuinely require confirmation, these confirmations are still presented far too often. If your security processes demand the patience of a Vulcan and the attention to detail of a leet-coder, you don’t have a security process. Demanding that developers approve each confirmation (especially when the confirmations are as obtuse as <code>Yes, and don’t ask again for: awk '{print length($0), $0}'</code> - what does that even mean?) has more in common with social engineering attacks like MFA fatigue than it does with a practical security process.</p>
<p>A better solution is to run AI agents in a sandboxed environment that limits their access via policies. This way, trusted prompts can be run without confirmation, with the assurance that the agent cannot access sensitive information or perform destructive actions.</p>
<p>The goal of the sandbox presented in this post is to:</p>
<ul>
<li>Enable a no-prompt experience for developers using local AI agents.</li>
<li>Grant full access to the source code checked out on the local machine.</li>
<li>Enable the local IDE MCP server to allow the AI agent to learn the currently opened file and perform tasks like compiling code and checking for errors.</li>
<li>Allow custom MCP servers to be run.</li>
<li>Provide a full suite of CLI tools for the AI agent to use.</li>
<li>Enable full internet access, albeit as an essentially unauthenticated client.</li>
<li>Deny access to any credentials that may be saved on the local machine.</li>
<li>Deny the ability to commit changes or push changes to any remote repository.</li>
<li>Deny the ability to install new software.</li>
</ul>
<p>Non-goals are:</p>
<ul>
<li>Guaranteeing that malicious or untrusted prompts will do no harm.</li>
<li>Providing an environment where untrusted LLMs can be run safely.</li>
<li>Always prioritizing security over convenience.</li>
</ul>
<p>We’ll focus on running Claude Code in the sandbox, but the same approach applies to other local AI agents.</p>
<p>To achieve these goals, the sandbox environment will be created as a Vagrant box.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>You can install the <code>vagrant</code> CLI from the <a href="https://developer.hashicorp.com/vagrant/install">Vagrant website</a>.</p>
<p>MacOS and Parallels users will need to install the <a href="https://parallels.github.io/vagrant-parallels/docs/installation/">Parallels provider</a>.</p>
<p>Linux users will need to install the <a href="https://vagrant-libvirt.github.io/vagrant-libvirt/#installation">libvirt provider</a>.</p>
<p>Windows users will need to use the <a href="https://developer.hashicorp.com/vagrant/docs/providers/virtualbox">VirtualBox provider</a> or the <a href="https://developer.hashicorp.com/vagrant/docs/providers/hyperv">Hyper-V provider</a>.</p>
<h2 id="creating-the-sandbox">Creating the sandbox</h2>
<p>The sandbox is coded in a <code>Vagrantfile</code> that defines how the virtual machine is created and configured.</p>
<h3 id="importing-required-libraries">Importing required libraries</h3>
<p>We’ll make use of the <code>shellwords</code> library to escape shell arguments when creating the sandbox:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#000000">require </span><span style="color:#A31515">"shellwords"</span></span></code></pre>
<h3 id="defining-global-constants">Defining global constants</h3>
<p>Vagrant requires a user with <code>sudo</code> privileges to execute the provisioning scripts. This user is called <code>vagrant</code> by default, and is present in most base Vagrant boxes.</p>
<p>So we need to create a restricted user for the AI agent. This user is named <code>claude</code> and has UID 1001. The home directory for this user is <code>/home/claude</code>, and the runtime directory is <code>/run/user/1001</code>:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#0070C1">AGENT_USER</span><span style="color:#000000">        = </span><span style="color:#A31515">"claude"</span></span>
<span class="line"><span style="color:#0070C1">AGENT_UID</span><span style="color:#000000">         = </span><span style="color:#098658">1001</span></span>
<span class="line"><span style="color:#0070C1">AGENT_HOME</span><span style="color:#000000">        = </span><span style="color:#A31515">"/home/</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_USER</span><span style="color:#0000FF">}</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#0070C1">AGENT_RUNTIME_DIR</span><span style="color:#000000"> = </span><span style="color:#A31515">"/run/user/</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_UID</span><span style="color:#0000FF">}</span><span style="color:#A31515">"</span></span></code></pre>
<h3 id="capturing-the-host-home-directory">Capturing the host home directory</h3>
<p>A challenge with the sandbox environment is that directories mounted from the host machine will appear in a different path. For example, project repositories mounted from <code>~/Code</code> on the host machine will appear in <code>/home/claude/Code</code> in the sandbox. We need to track the directory the files are mounted from so we can instruct the AI agent to translate paths reported by the IDE to the correct paths in the sandbox. The host home directory is defined as follows:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#0070C1">HOST_HOME</span><span style="color:#000000"> = </span><span style="color:#267F99">File</span><span style="color:#000000">.</span><span style="color:#795E26">expand_path</span><span style="color:#000000">(</span><span style="color:#A31515">"~"</span><span style="color:#000000">)</span></span></code></pre>
<h3 id="defining-the-base-box">Defining the base box</h3>
<p>We start a Vagrant configuration block and define the base box to use. In this case, we use the <code>bento/ubuntu-24.04</code> box, which is a minimal Ubuntu 24.04 image:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#267F99">Vagrant</span><span style="color:#000000">.</span><span style="color:#795E26">configure</span><span style="color:#000000">(</span><span style="color:#A31515">"2"</span><span style="color:#000000">) </span><span style="color:#AF00DB">do</span><span style="color:#000000"> |</span><span style="color:#001080">config</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">box</span><span style="color:#000000"> = </span><span style="color:#A31515">"bento/ubuntu-24.04"</span></span></code></pre>
<p>Windows users will need to select a different base box, as the <code>bento/ubuntu-24.04</code> box is not compatible with Hyper-V. We use the <code>boxen/ubuntu-24.04</code> box for Hyper-V:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "hyperv"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">hv</span><span style="color:#000000">, </span><span style="color:#001080">override</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    override.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">box</span><span style="color:#000000"> = </span><span style="color:#A31515">"boxen/ubuntu-24.04"</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span></code></pre>
<div class="info"><p>The public Vagrant Cloud boxes are being <a href="https://developer.hashicorp.com/hcp/docs/vagrant/hcp-vagrant-eol">deprecated</a>. You will need to eventually source the base boxes from your own file storage.</p></div>
<h3 id="configuring-directory-mounts">Configuring directory mounts</h3>
<p>Vagrant automatically mounts the current directory to <code>/vagrant</code> in the virtual machine. We disable this mount as we will only be exposing the <code>~/Code</code> directory to the sandbox, and we don’t want the AI agent to have access to unexpected files:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">synced_folder</span><span style="color:#A31515"> "."</span><span style="color:#000000">, </span><span style="color:#A31515">"/vagrant"</span><span style="color:#000000">, </span><span style="color:#0000FF">disabled:</span><span style="color:#0000FF"> true</span></span></code></pre>
<p>We mount the <code>~/Code</code> directory to <code>/home/claude/Code</code> in the sandbox, using NFS for better performance. We also disable UDP for NFS, as it can cause issues with some network configurations:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">synced_folder</span><span style="color:#267F99"> File</span><span style="color:#000000">.</span><span style="color:#795E26">expand_path</span><span style="color:#000000">(</span><span style="color:#A31515">"~/Code"</span><span style="color:#000000">), </span><span style="color:#A31515">"</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_HOME</span><span style="color:#0000FF">}</span><span style="color:#A31515">/Code"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    type:</span><span style="color:#A31515"> "nfs"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    nfs_version:</span><span style="color:#098658"> 3</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    nfs_udp:</span><span style="color:#0000FF"> false</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    mount_options:</span><span style="color:#000000"> [</span><span style="color:#A31515">"actimeo=1"</span><span style="color:#000000">, </span><span style="color:#A31515">"nolock"</span><span style="color:#000000">, </span><span style="color:#A31515">"tcp"</span><span style="color:#000000">, </span><span style="color:#A31515">"rw"</span><span style="color:#000000">, </span><span style="color:#A31515">"fsc"</span><span style="color:#000000">]</span></span></code></pre>
<p>When creating a virtual machine on macOS and Parallels or Windows and Hyper-V, the in-built shared folder implementation is more stable than NFS. We can override the NFS mount and use the native mount options:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "parallels"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">prl</span><span style="color:#000000">, </span><span style="color:#001080">override</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    override.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">synced_folder</span><span style="color:#267F99"> File</span><span style="color:#000000">.</span><span style="color:#795E26">expand_path</span><span style="color:#000000">(</span><span style="color:#A31515">"~/Code"</span><span style="color:#000000">), </span><span style="color:#A31515">"</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_HOME</span><span style="color:#0000FF">}</span><span style="color:#A31515">/Code"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    type:</span><span style="color:#0000FF"> nil</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    mount_options:</span><span style="color:#000000"> [</span><span style="color:#A31515">"share"</span><span style="color:#000000">, </span><span style="color:#A31515">"rw"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "hyperv"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">hv</span><span style="color:#000000">, </span><span style="color:#001080">override</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    override.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">synced_folder</span><span style="color:#267F99"> File</span><span style="color:#000000">.</span><span style="color:#795E26">expand_path</span><span style="color:#000000">(</span><span style="color:#A31515">"~/Code"</span><span style="color:#000000">), </span><span style="color:#A31515">"</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_HOME</span><span style="color:#0000FF">}</span><span style="color:#A31515">/Code"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    type:</span><span style="color:#A31515"> "smb"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    mount_options:</span><span style="color:#000000"> [</span><span style="color:#A31515">"rw"</span><span style="color:#000000">, </span><span style="color:#A31515">"uid=</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_UID</span><span style="color:#0000FF">}</span><span style="color:#A31515">"</span><span style="color:#000000">, </span><span style="color:#A31515">"gid=</span><span style="color:#0000FF">#{</span><span style="color:#0070C1">AGENT_UID</span><span style="color:#0000FF">}</span><span style="color:#A31515">"</span><span style="color:#000000">, </span><span style="color:#A31515">"mfsymlinks"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span></code></pre>
<h3 id="setting-the-virtual-machine-resources">Setting the virtual machine resources</h3>
<p>The sandbox is configured with 4GB of memory and 6 CPUs. This is sufficient for most local AI agents, but you can adjust these values as needed:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "parallels"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">prl</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    prl.</span><span style="color:#795E26">memory</span><span style="color:#000000"> = </span><span style="color:#098658">4096</span></span>
<span class="line"><span style="color:#000000">    prl.</span><span style="color:#795E26">cpus</span><span style="color:#000000">   = </span><span style="color:#098658">6</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "libvirt"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">lv</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    lv.</span><span style="color:#795E26">memory</span><span style="color:#000000"> = </span><span style="color:#098658">4096</span></span>
<span class="line"><span style="color:#000000">    lv.</span><span style="color:#795E26">cpus</span><span style="color:#000000">   = </span><span style="color:#098658">6</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "virtualbox"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">vb</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    vb.</span><span style="color:#795E26">memory</span><span style="color:#000000"> = </span><span style="color:#098658">4096</span></span>
<span class="line"><span style="color:#000000">    vb.</span><span style="color:#795E26">cpus</span><span style="color:#000000">   = </span><span style="color:#098658">6</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provider</span><span style="color:#A31515"> "hyperv"</span><span style="color:#AF00DB"> do</span><span style="color:#000000"> |</span><span style="color:#001080">hv</span><span style="color:#000000">|</span></span>
<span class="line"><span style="color:#000000">    hv.</span><span style="color:#795E26">maxmemory</span><span style="color:#000000"> = </span><span style="color:#098658">4096</span></span>
<span class="line"><span style="color:#000000">    hv.</span><span style="color:#795E26">cpus</span><span style="color:#000000">   = </span><span style="color:#098658">6</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span></code></pre>
<h3 id="exposing-the-anthropic-api-key">Exposing the Anthropic API key</h3>
<p>The AI agent needs an API key to authenticate with Claude. We fetch the API key from the host environment and expose it in a file called <code>/etc/anthropic_api_key.env</code> in the sandbox. This file is owned by root and has permissions set to 600, so only root can read it. The AI agent will be able to read this file, but it will not be able to write to it or delete it:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#001080">anthropic_api_key</span><span style="color:#000000"> = </span><span style="color:#267F99">ENV</span><span style="color:#000000">.</span><span style="color:#795E26">fetch</span><span style="color:#000000">(</span><span style="color:#A31515">'ANTHROPIC_API_KEY'</span><span style="color:#000000">) </span><span style="color:#AF00DB">do</span></span>
<span class="line"><span style="color:#0000FF">    raise</span><span style="color:#A31515"> "ANTHROPIC_API_KEY is not set on the host. "</span><span style="color:#000000"> \</span></span>
<span class="line"><span style="color:#A31515">          "Export it before running vagrant up:</span><span style="color:#EE0000">\n</span><span style="color:#A31515">"</span><span style="color:#000000"> \</span></span>
<span class="line"><span style="color:#A31515">          "  export ANTHROPIC_API_KEY='your-key-here'"</span></span>
<span class="line"><span style="color:#AF00DB">  end</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">  config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provision</span><span style="color:#A31515"> "shell"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    run:</span><span style="color:#A31515"> "always"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    upload_path:</span><span style="color:#A31515"> "/home/vagrant/vagrant-shell"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    inline:</span><span style="color:#A31515"> &#x3C;&#x3C;-SHELL</span></span>
<span class="line"><span style="color:#795E26">    set</span><span style="color:#0000FF"> -euo</span><span style="color:#A31515"> pipefail</span></span>
<span class="line"><span style="color:#795E26">    install</span><span style="color:#0000FF"> -o</span><span style="color:#A31515"> root</span><span style="color:#0000FF"> -g</span><span style="color:#A31515"> root</span><span style="color:#0000FF"> -m</span><span style="color:#098658"> 600</span><span style="color:#A31515"> /dev/null</span><span style="color:#A31515"> /etc/anthropic_api_key.env</span></span>
<span class="line"><span style="color:#795E26">    echo</span><span style="color:#A31515"> "export ANTHROPIC_API_KEY='#{anthropic_api_key}'"</span><span style="color:#000000"> ></span><span style="color:#A31515"> /etc/anthropic_api_key.env</span></span>
<span class="line"><span style="color:#A31515">  SHELL</span></span></code></pre>
<div class="info"><p>A common challenge in building sandbox environments is exposing secrets required to support the AI agent or MCP servers. While we’ll make efforts to hide these credentials from the AI agent, the agent can still exfiltrate them, as we’ll see later. This is where we are forced to trade off between security and convenience. This sandbox makes a conscious decision to prioritize convenience.</p></div>
<h3 id="copying-the-claude-configuration">Copying the Claude configuration</h3>
<p>The Claude configuration is copied from the host machine to the sandbox. This allows the AI agent to use the same configuration as the host machine, while still being restricted to the sandbox environment:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provision</span><span style="color:#A31515"> "file"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    source:</span><span style="color:#A31515"> "~/.claude.json"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    destination:</span><span style="color:#A31515"> "/home/vagrant/claude.json.upload"</span></span></code></pre>
<p>We now start building the sandbox environment. This is done in a shell provisioner that runs as root:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="ruby"><code><span class="line"><span style="color:#000000">config.</span><span style="color:#795E26">vm</span><span style="color:#000000">.</span><span style="color:#795E26">provision</span><span style="color:#A31515"> "shell"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    upload_path:</span><span style="color:#A31515"> "/home/vagrant/vagrant-shell"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0000FF">    inline:</span><span style="color:#A31515"> &#x3C;&#x3C;-SHELL</span></span>
<span class="line"><span style="color:#795E26">    set</span><span style="color:#0000FF"> -euo</span><span style="color:#A31515"> pipefail</span></span></code></pre>
<h3 id="create-the-claude-user">Create the claude user</h3>
<p>The <code>claude</code> user is created with the specified UID, home directory, and shell. The <code>-M</code> option prevents the creation of a home directory, as we will construct this manually:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    useradd</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#0000FF">      --uid</span><span style="color:#008000"> #{AGENT_UID} \</span></span>
<span class="line"><span style="color:#795E26">      --home-dir</span><span style="color:#008000"> #{AGENT_HOME} \</span></span>
<span class="line"><span style="color:#795E26">      --shell</span><span style="color:#A31515"> /bin/bash</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#0000FF">      -M</span><span style="color:#008000"> #{AGENT_USER}</span></span></code></pre>
<p>The home directory for the <code>claude</code> user is created with the correct ownership and permissions. The <code>-d</code> option creates the directory, the <code>-o</code> and <code>-g</code> options set the owner and group to the <code>claude</code> user, and the <code>-m</code> option sets the permissions to 750, which allows the owner to read, write, and execute, while allowing the group to read and execute, but not write:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    install</span><span style="color:#0000FF"> -d</span><span style="color:#0000FF"> -o</span><span style="color:#008000"> #{AGENT_USER} -g #{AGENT_USER} -m 750 #{AGENT_HOME}</span></span></code></pre>
<p>Launching the AI agent requires that the <code>ANTHROPIC_API_KEY</code> environment variable be set. We create a launcher script that sets this environment variable and then launches the AI agent as the <code>claude</code> user. The launcher script is owned by root and has permissions set to 755, so it can be executed by any user. This is how we prevent the <code>claude</code> user from reading the contents of the <code>/etc/anthropic_api_key.env</code> file, while still allowing the AI agent to authenticate itself with the <code>ANTHROPIC_API_KEY</code> environment variable:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    cat</span><span style="color:#000000"> > </span><span style="color:#A31515">/usr/local/sbin/claude-agent</span><span style="color:#000000"> &#x3C;&#x3C;</span><span style="color:#000000">'LAUNCHER'</span></span>
<span class="line"><span style="color:#A31515">#!/bin/bash</span></span>
<span class="line"><span style="color:#A31515">set -euo pipefail</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">. /etc/anthropic_api_key.env</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515"># --dir is the directory the agent should start in, given relative to the synced</span></span>
<span class="line"><span style="color:#A31515"># tree. claude.sh sends the directory it was called from on the host, which is the</span></span>
<span class="line"><span style="color:#A31515"># same tree under a different prefix, so the relative path is all that travels.</span></span>
<span class="line"><span style="color:#A31515"># Optional: without it the agent starts at the root, which is what a bare</span></span>
<span class="line"><span style="color:#A31515"># `sudo /usr/local/sbin/claude-agent` in the guest still does. Anything left on the</span></span>
<span class="line"><span style="color:#A31515"># command line afterwards is passed through to claude untouched.</span></span>
<span class="line"><span style="color:#A31515">code_root=#{AGENT_HOME}/Code</span></span>
<span class="line"><span style="color:#A31515">target=$code_root</span></span>
<span class="line"><span style="color:#A31515">rel=</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">if [ "${1:-}" = --dir ]; then</span></span>
<span class="line"><span style="color:#A31515">  if [ "$#" -lt 2 ]; then</span></span>
<span class="line"><span style="color:#A31515">    echo "claude-agent: --dir needs a value" >&#x26;2</span></span>
<span class="line"><span style="color:#A31515">    exit 2</span></span>
<span class="line"><span style="color:#A31515">  fi</span></span>
<span class="line"><span style="color:#A31515">  rel=$2</span></span>
<span class="line"><span style="color:#A31515">  shift 2</span></span>
<span class="line"><span style="color:#A31515">fi</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515"># Validated, but never fatal: a --dir that cannot be honoured should still get you a</span></span>
<span class="line"><span style="color:#A31515"># working agent at the root rather than no agent at all. The one thing worth being</span></span>
<span class="line"><span style="color:#A31515"># strict about is the shape — --dir names a location inside the synced tree by</span></span>
<span class="line"><span style="color:#A31515"># construction, so an absolute path or a .. component is a caller bug, and a caller</span></span>
<span class="line"><span style="color:#A31515"># bug that silently starts the agent somewhere outside the tree is worth refusing.</span></span>
<span class="line"><span style="color:#A31515">case $rel in</span></span>
<span class="line"><span style="color:#A31515">  ""|.)</span></span>
<span class="line"><span style="color:#A31515">    ;;</span></span>
<span class="line"><span style="color:#A31515">  /*)</span></span>
<span class="line"><span style="color:#A31515">    echo "claude-agent: --dir must be relative to $code_root, ignoring '$rel'" >&#x26;2</span></span>
<span class="line"><span style="color:#A31515">    ;;</span></span>
<span class="line"><span style="color:#A31515">  ..|../*|*/..|*/../*)</span></span>
<span class="line"><span style="color:#A31515">    echo "claude-agent: --dir must stay inside $code_root, ignoring '$rel'" >&#x26;2</span></span>
<span class="line"><span style="color:#A31515">    ;;</span></span>
<span class="line"><span style="color:#A31515">  *)</span></span>
<span class="line"><span style="color:#A31515">    if [ -d "$code_root/$rel" ]; then</span></span>
<span class="line"><span style="color:#A31515">      target=$code_root/$rel</span></span>
<span class="line"><span style="color:#A31515">    else</span></span>
<span class="line"><span style="color:#A31515">      echo "claude-agent: $code_root/$rel does not exist, starting in $code_root" >&#x26;2</span></span>
<span class="line"><span style="color:#A31515">    fi</span></span>
<span class="line"><span style="color:#A31515">    ;;</span></span>
<span class="line"><span style="color:#A31515">esac</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515"># The target is handed to the inner shell as a positional argument rather than</span></span>
<span class="line"><span style="color:#A31515"># spliced into its script. That script is a single-quoted string, so a path pasted</span></span>
<span class="line"><span style="color:#A31515"># into it would be parsed by that shell as code.</span></span>
<span class="line"><span style="color:#A31515">exec sudo -u #{AGENT_USER} -H env ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \</span></span>
<span class="line"><span style="color:#A31515">  bash -lc 'cd "$1" || exit 1; shift; exec claude "$@"' claude "$target" "$@"</span></span>
<span class="line"><span style="color:#000000">LAUNCHER</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#A31515"> root:root</span><span style="color:#A31515"> /usr/local/sbin/claude-agent</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 755</span><span style="color:#A31515"> /usr/local/sbin/claude-agent</span></span></code></pre>
<p>The <code>claude</code> user’s home directory is currently empty. We copy the contents of <code>/etc/skel</code> to the <code>claude</code> user’s home directory. This includes files like <code>.bashrc</code>, <code>.profile</code>, and <code>.bash_logout</code>, which are used to configure the shell environment for the user:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#AF00DB">    for</span><span style="color:#001080"> skel</span><span style="color:#AF00DB"> in</span><span style="color:#A31515"> /etc/skel/.[!.</span><span style="color:#000000">]*; </span><span style="color:#AF00DB">do</span></span>
<span class="line"><span style="color:#000000">      [ -f </span><span style="color:#A31515">"</span><span style="color:#001080">$skel</span><span style="color:#A31515">"</span><span style="color:#000000"> ] || </span><span style="color:#AF00DB">continue</span></span>
<span class="line"><span style="color:#795E26">      install</span><span style="color:#0000FF"> -o</span><span style="color:#008000"> #{AGENT_USER} -g #{AGENT_USER} -m 644 \</span></span>
<span class="line"><span style="color:#795E26">        "</span><span style="color:#001080">$skel</span><span style="color:#795E26">"</span><span style="color:#A31515"> "#{AGENT_HOME}/$(</span><span style="color:#795E26">basename</span><span style="color:#A31515"> "</span><span style="color:#001080">$skel</span><span style="color:#A31515">")"</span></span>
<span class="line"><span style="color:#AF00DB">    done</span></span></code></pre>
<p>The Claude configuration file is copied to the <code>claude</code> user’s home directory. The file is owned by the <code>claude</code> user and has permissions set to 600, so only the owner can read and write to the file. The original file in <code>/home/vagrant/claude.json.upload</code> is then cleaned up:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    install</span><span style="color:#0000FF"> -o</span><span style="color:#008000"> #{AGENT_USER} -g #{AGENT_USER} -m 600 \</span></span>
<span class="line"><span style="color:#795E26">      /home/vagrant/claude.json.upload</span><span style="color:#008000"> #{AGENT_HOME}/.claude.json</span></span>
<span class="line"><span style="color:#795E26">    rm</span><span style="color:#0000FF"> -f</span><span style="color:#A31515"> /home/vagrant/claude.json.upload</span></span></code></pre>
<h3 id="configuring-claude">Configuring Claude</h3>
<p>We now configure the Claude Code managed settings. These settings are stored in <code>/etc/claude-code/managed-settings.json</code>, which is owned by root and has permissions set to 444, so it can be read by any user, but not written to.</p>
<p>The permissions deny the ability to commit or add files to a Git repository, as well as the ability to execute certain commands in IntelliJ. The settings also disable sideload flags and restrict the AI agent’s access to certain environment variables and files. It also excludes <code>docker</code> commands from the sandbox, which is required to allow the AI agent to run Docker commands:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    mkdir</span><span style="color:#0000FF"> -p</span><span style="color:#A31515"> /etc/claude-code</span></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#A31515"> root:root</span><span style="color:#A31515"> /etc/claude-code</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 755</span><span style="color:#A31515"> /etc/claude-code</span></span>
<span class="line"><span style="color:#795E26">    cat</span><span style="color:#000000"> > </span><span style="color:#A31515">/etc/claude-code/managed-settings.json</span><span style="color:#000000"> &#x3C;&#x3C;</span><span style="color:#000000">'JSON'</span></span>
<span class="line"><span style="color:#A31515">{</span></span>
<span class="line"><span style="color:#A31515">  "permissions": {</span></span>
<span class="line"><span style="color:#A31515">    "deny": [</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__execute_terminal_command",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__execute_run_configuration",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__execute_tool",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__build_project",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__run_inspection_kts",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__validate_inspection_kts",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__execute_sql_query",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_start_debugger_session",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_control_session",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_evaluate_expression",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_set_variable",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_set_breakpoint",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_remove_breakpoint",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__xdebug_run_to_line",</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__apply_patch",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__create_new_file",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__reformat_file",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__rename_refactoring",</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__create_database_connection",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__edit_database_connection",</span></span>
<span class="line"><span style="color:#A31515">      "mcp__intellij__test_database_connection",</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">      "Bash(git add)",</span></span>
<span class="line"><span style="color:#A31515">      "Bash(git add:*)",</span></span>
<span class="line"><span style="color:#A31515">      "Bash(git commit)",</span></span>
<span class="line"><span style="color:#A31515">      "Bash(git commit:*)"</span></span>
<span class="line"><span style="color:#A31515">    ]</span></span>
<span class="line"><span style="color:#A31515">  },</span></span>
<span class="line"><span style="color:#A31515">  "allowManagedPermissionRulesOnly": true,</span></span>
<span class="line"><span style="color:#A31515">  "allowManagedHooksOnly": true,</span></span>
<span class="line"><span style="color:#A31515">  "disableSideloadFlags": true,</span></span>
<span class="line"><span style="color:#A31515">  "env": {</span></span>
<span class="line"><span style="color:#A31515">    "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB": "0"</span></span>
<span class="line"><span style="color:#A31515">  },</span></span>
<span class="line"><span style="color:#A31515">  "sandbox": {</span></span>
<span class="line"><span style="color:#A31515">    "enabled": true,</span></span>
<span class="line"><span style="color:#A31515">    "allowUnsandboxedCommands": false,</span></span>
<span class="line"><span style="color:#A31515">    "excludedCommands": [</span></span>
<span class="line"><span style="color:#A31515">      "docker *"</span></span>
<span class="line"><span style="color:#A31515">    ],</span></span>
<span class="line"><span style="color:#A31515">    "allowManagedReadPathsOnly": true,</span></span>
<span class="line"><span style="color:#A31515">    "filesystem": {</span></span>
<span class="line"><span style="color:#A31515">      "denyRead": [</span></span>
<span class="line"><span style="color:#A31515">        "/etc/*.env",</span></span>
<span class="line"><span style="color:#A31515">        "#{AGENT_HOME}/.claude.json"</span></span>
<span class="line"><span style="color:#A31515">      ],</span></span>
<span class="line"><span style="color:#A31515">      "denyWrite": [</span></span>
<span class="line"><span style="color:#A31515">        "#{AGENT_HOME}/.claude.json",</span></span>
<span class="line"><span style="color:#A31515">        "#{AGENT_HOME}/.claude/settings*.json",</span></span>
<span class="line"><span style="color:#A31515">        "#{AGENT_HOME}/.claude/CLAUDE.md",</span></span>
<span class="line"><span style="color:#A31515">        "#{AGENT_HOME}/Code/.claude/settings*.json"</span></span>
<span class="line"><span style="color:#A31515">      ]</span></span>
<span class="line"><span style="color:#A31515">    },</span></span>
<span class="line"><span style="color:#A31515">    "credentials": {</span></span>
<span class="line"><span style="color:#A31515">      "files": [</span></span>
<span class="line"><span style="color:#A31515">        { "path": "/etc/anthropic_api_key.env", "mode": "deny" },</span></span>
<span class="line"><span style="color:#A31515">        { "path": "/etc/github_copilot_token.env", "mode": "deny" },</span></span>
<span class="line"><span style="color:#A31515">        { "path": "#{AGENT_HOME}/.claude/settings.json", "mode": "deny" }</span></span>
<span class="line"><span style="color:#A31515">      ],</span></span>
<span class="line"><span style="color:#A31515">      "envVars": [</span></span>
<span class="line"><span style="color:#A31515">        { "name": "ANTHROPIC_API_KEY", "mode": "deny" }</span></span>
<span class="line"><span style="color:#A31515">      ]</span></span>
<span class="line"><span style="color:#A31515">    }</span></span>
<span class="line"><span style="color:#A31515">  }</span></span>
<span class="line"><span style="color:#A31515">}</span></span>
<span class="line"><span style="color:#000000">JSON</span></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#A31515"> root:root</span><span style="color:#A31515"> /etc/claude-code/managed-settings.json</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 444</span><span style="color:#A31515"> /etc/claude-code/managed-settings.json</span></span></code></pre>
<div class="info"><p>Again, we see a trade-off between security and convenience, as we mostly trust the IntelliJ MCP server. This MCP server is powerful and grants extensive access. Some tools have been denied, but the AI agent still has a broad collection of tools to use.</p></div>
<p>The Claude user settings are defined in <code>/home/claude/.claude/settings.json</code>, effectively disabling all security prompts:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    mkdir</span><span style="color:#0000FF"> -p</span><span style="color:#008000"> #{AGENT_HOME}/.claude</span></span>
<span class="line"><span style="color:#795E26">    cat</span><span style="color:#000000"> > </span><span style="color:#008000">#{AGENT_HOME}/.claude/settings.json &#x3C;&#x3C;'JSON'</span></span>
<span class="line"><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#795E26">  "skipDangerousModePermissionPrompt"</span><span style="color:#795E26">:</span><span style="color:#0000FF"> true</span><span style="color:#A31515">,</span></span>
<span class="line"><span style="color:#795E26">  "acceptEdits"</span><span style="color:#795E26">:</span><span style="color:#0000FF"> true</span><span style="color:#A31515">,</span></span>
<span class="line"><span style="color:#795E26">  "permissions"</span><span style="color:#795E26">:</span><span style="color:#A31515"> {</span></span>
<span class="line"><span style="color:#795E26">    "defaultMode"</span><span style="color:#795E26">:</span><span style="color:#A31515"> "bypassPermissions"</span></span>
<span class="line"><span style="color:#000000">  },</span></span>
<span class="line"><span style="color:#795E26">  "sandbox"</span><span style="color:#795E26">:</span><span style="color:#A31515"> {</span></span>
<span class="line"><span style="color:#795E26">    "autoAllowBashIfSandboxed"</span><span style="color:#795E26">:</span><span style="color:#0000FF"> true</span></span>
<span class="line"><span style="color:#000000">  }</span></span>
<span class="line"><span style="color:#000000">}</span></span>
<span class="line"><span style="color:#795E26">JSON</span></span></code></pre>
<h3 id="providing-custom-instructions-to-the-ai-agent">Providing custom instructions to the AI agent</h3>
<p>Custom instructions are provided to the AI agent in a file called <code>CLAUDE.md</code>. This file is owned by the <code>claude</code> user and has permissions set to 644, so it can be read by any user but written to only by the owner. The instructions explain how to translate paths from the host machine to the sandbox environment, and how to use guest paths for tool calls:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    cat</span><span style="color:#000000"> > </span><span style="color:#008000">#{AGENT_HOME}/.claude/CLAUDE.md &#x3C;&#x3C;'MARKDOWN'</span></span>
<span class="line"><span style="color:#008000"># Filesystem paths in this sandbox</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">You</span><span style="color:#A31515"> are</span><span style="color:#A31515"> running</span><span style="color:#A31515"> inside</span><span style="color:#A31515"> a</span><span style="color:#A31515"> Vagrant</span><span style="color:#A31515"> guest</span><span style="color:#A31515"> VM.</span><span style="color:#A31515"> The</span><span style="color:#A31515"> user,</span><span style="color:#A31515"> their</span><span style="color:#A31515"> IDE,</span><span style="color:#A31515"> and</span><span style="color:#A31515"> their</span></span>
<span class="line"><span style="color:#795E26">terminal</span><span style="color:#A31515"> are</span><span style="color:#A31515"> on</span><span style="color:#A31515"> the</span><span style="color:#0000FF"> *</span><span style="color:#A31515">host</span><span style="color:#0000FF">*</span><span style="color:#A31515"> machine.</span><span style="color:#A31515"> The</span><span style="color:#A31515"> host</span><span style="color:#A31515"> directory</span><span style="color:#A31515"> `#{HOST_HOME}/Code` is</span></span>
<span class="line"><span style="color:#795E26">synced</span><span style="color:#A31515"> to `</span><span style="color:#000000">#{AGENT_HOME}/Code` in this guest — same files, different prefix.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">Any</span><span style="color:#A31515"> path</span><span style="color:#A31515"> that</span><span style="color:#A31515"> reaches</span><span style="color:#A31515"> you</span><span style="color:#A31515"> from</span><span style="color:#A31515"> the</span><span style="color:#A31515"> host</span><span style="color:#A31515"> side</span><span style="color:#A31515"> uses</span><span style="color:#A31515"> the</span><span style="color:#A31515"> host</span><span style="color:#A31515"> prefix</span><span style="color:#A31515"> and</span><span style="color:#A31515"> is</span><span style="color:#A31515"> NOT</span></span>
<span class="line"><span style="color:#795E26">valid</span><span style="color:#A31515"> here.</span><span style="color:#A31515"> This</span><span style="color:#A31515"> includes:</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">-</span><span style="color:#A31515"> the</span><span style="color:#A31515"> path</span><span style="color:#A31515"> of</span><span style="color:#A31515"> the</span><span style="color:#A31515"> file</span><span style="color:#A31515"> currently</span><span style="color:#A31515"> open</span><span style="color:#A31515"> in</span><span style="color:#A31515"> the</span><span style="color:#A31515"> user's IDE</span></span>
<span class="line"><span style="color:#A31515">- paths in IDE diagnostics, selections, or attached editor context</span></span>
<span class="line"><span style="color:#A31515">- paths the user types or pastes, and paths in output copied from the host</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">## Translate before every tool call</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">Rewrite the prefix, keep the rest of the path unchanged:</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">| Host path | Guest path to use |</span></span>
<span class="line"><span style="color:#A31515">| --- | --- |</span></span>
<span class="line"><span style="color:#A31515">| `#{HOST_HOME}/Code/&#x3C;rest>` | `#{AGENT_HOME}/Code/&#x3C;rest>` |</span></span>
<span class="line"><span style="color:#A31515">| `~/Code/&#x3C;rest>` | `#{AGENT_HOME}/Code/&#x3C;rest>` |</span></span>
<span class="line"><span style="color:#A31515">| `#{HOST_HOME}/&#x3C;rest>` (outside `Code`) | not available in this sandbox |</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">For example, if the IDE reports the open file as</span></span>
<span class="line"><span style="color:#A31515">`#{HOST_HOME}/Code/MyProject/src/main.ts`, read and edit</span></span>
<span class="line"><span style="color:#A31515">`#{AGENT_HOME}/Code/MyProject/src/main.ts`.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">Only `~/Code` is synced. If a host path falls outside it, do not invent a guest</span></span>
<span class="line"><span style="color:#A31515">equivalent and do not create the directory to make the path resolve — say the</span></span>
<span class="line"><span style="color:#A31515">file is not mounted into the sandbox and ask the user how to proceed.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">## Translating back</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">Use guest paths for every tool call, and when you quote a path in your answer.</span></span>
<span class="line"><span style="color:#A31515">The exception is when you are telling the user which file to open on the host</span></span>
<span class="line"><span style="color:#A31515">(so their IDE can resolve it) — give the `#{HOST_HOME}/...` form there, and say</span></span>
<span class="line"><span style="color:#A31515">which side of the mapping the path belongs to.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">The synced folder is mounted read-write, so edits you make under</span></span>
<span class="line"><span style="color:#A31515">`#{AGENT_HOME}/Code` appear on the host immediately. These are the user's</span><span style="color:#A31515"> real</span></span>
<span class="line"><span style="color:#795E26">working</span><span style="color:#A31515"> files,</span><span style="color:#A31515"> not</span><span style="color:#A31515"> a</span><span style="color:#A31515"> throwaway</span><span style="color:#A31515"> copy</span><span style="color:#A31515"> —</span><span style="color:#A31515"> treat</span><span style="color:#A31515"> them</span><span style="color:#A31515"> accordingly.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># The account you are running as</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">You</span><span style="color:#A31515"> are</span><span style="color:#A31515"> the</span><span style="color:#A31515"> `#{AGENT_USER}` user. It is unprivileged on purpose: it has no sudo, no</span></span>
<span class="line"><span style="color:#795E26">password,</span><span style="color:#A31515"> and no membership of the `</span><span style="color:#795E26">sudo</span><span style="color:#A31515">`</span><span style="color:#795E26">,</span><span style="color:#A31515"> `</span><span style="color:#795E26">docker</span><span style="color:#A31515">`</span><span style="color:#795E26">,</span><span style="color:#A31515"> `</span><span style="color:#795E26">lxd</span><span style="color:#A31515">` </span><span style="color:#795E26">or</span><span style="color:#A31515"> `</span><span style="color:#795E26">adm</span><span style="color:#A31515">` </span><span style="color:#795E26">groups.</span><span style="color:#A31515"> The</span></span>
<span class="line"><span style="color:#A31515">`</span><span style="color:#795E26">vagrant</span><span style="color:#A31515">` </span><span style="color:#795E26">account,</span><span style="color:#A31515"> and its home directory, are not yours to read or write.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">So:</span><span style="color:#A31515"> install nothing system-wide. `</span><span style="color:#795E26">apt-get</span><span style="color:#A31515">`</span><span style="color:#795E26">,</span><span style="color:#A31515"> `</span><span style="color:#795E26">npm</span><span style="color:#A31515"> install</span><span style="color:#0000FF"> -g</span><span style="color:#A31515">` </span><span style="color:#795E26">and</span><span style="color:#A31515"> anything else</span></span>
<span class="line"><span style="color:#795E26">needing</span><span style="color:#A31515"> root will fail, and that is the configuration working, not a problem to</span></span>
<span class="line"><span style="color:#795E26">route</span><span style="color:#A31515"> around. Use a venv, `</span><span style="color:#795E26">npm</span><span style="color:#A31515"> install` </span><span style="color:#795E26">into</span><span style="color:#A31515"> the project, or the rootless Docker</span></span>
<span class="line"><span style="color:#795E26">daemon</span><span style="color:#A31515"> already running for you (`</span><span style="color:#795E26">DOCKER_HOST</span><span style="color:#A31515">` </span><span style="color:#795E26">is</span><span style="color:#A31515"> set in your environment). If a</span></span>
<span class="line"><span style="color:#795E26">task</span><span style="color:#A31515"> genuinely needs root in this VM, say so and ask the user to run it from the</span></span>
<span class="line"><span style="color:#795E26">host</span><span style="color:#A31515"> with `</span><span style="color:#795E26">vagrant</span><span style="color:#A31515"> ssh`</span><span style="color:#795E26">.</span></span>
<span class="line"><span style="color:#795E26">MARKDOWN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#0000FF"> -R</span><span style="color:#008000"> #{AGENT_USER}:#{AGENT_USER} #{AGENT_HOME}/.claude</span></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#A31515"> root:root </span><span style="color:#008000">#{AGENT_HOME}/.claude/settings.json #{AGENT_HOME}/.claude/CLAUDE.md</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 444</span><span style="color:#008000"> #{AGENT_HOME}/.claude/settings.json</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 444</span><span style="color:#008000"> #{AGENT_HOME}/.claude/CLAUDE.md</span></span></code></pre>
<h3 id="creating-the-project-root-marker">Creating the project root marker</h3>
<p>Claude expects to find a <code>.mcp.json</code> file that marks the project’s root. We create an empty <code>.mcp.json</code> file in the sandbox home directory, owned by root and with permissions set to 444, so it can be read by any user, but not written to:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    touch</span><span style="color:#A31515"> /home/.mcp.json</span></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#A31515"> root:root</span><span style="color:#A31515"> /home/.mcp.json</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 444</span><span style="color:#A31515"> /home/.mcp.json</span></span></code></pre>
<h3 id="installing-supporting-tools">Installing supporting tools</h3>
<p>The OS is updated, and a set of tools is installed that the AI agent can use. These tools are installed system-wide, but the <code>claude</code> user does not have permission to install additional tools:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    apt-get</span><span style="color:#A31515"> update</span><span style="color:#0000FF"> -y</span></span>
<span class="line"><span style="color:#795E26">    apt-get</span><span style="color:#A31515"> upgrade</span><span style="color:#0000FF"> -y</span></span>
<span class="line"><span style="color:#795E26">    apt-get</span><span style="color:#A31515"> install</span><span style="color:#0000FF"> -y</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      auditd</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      binfmt-support</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      build-essential</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      curl</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      dbus-user-session</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      fuse-overlayfs</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      git</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      jq</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      python3</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      python3-pip</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      python3-venv</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      qemu-user-static</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      screen</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      slirp4netns</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      uidmap</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      unzip</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      ufw</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      btop</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      bubblewrap</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      socat</span></span></code></pre>
<h3 id="installing-rootless-docker">Installing rootless Docker</h3>
<p>Docker is installed in <a href="https://docs.docker.com/engine/security/rootless/">rootless mode</a>, so the <code>claude</code> user can run Docker commands without needing to use <code>sudo</code>. The Docker daemon is launched automatically when the sandbox is started, and the <code>DOCKER_HOST</code> environment variable is set to point to the rootless Docker daemon:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    install</span><span style="color:#0000FF"> -m</span><span style="color:#098658"> 0755</span><span style="color:#0000FF"> -d</span><span style="color:#A31515"> /etc/apt/keyrings</span></span>
<span class="line"><span style="color:#795E26">    curl</span><span style="color:#0000FF"> -fsSL</span><span style="color:#A31515"> https://download.docker.com/linux/ubuntu/gpg</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#000000">      | </span><span style="color:#795E26">dd</span><span style="color:#A31515"> of=/etc/apt/keyrings/docker.asc</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#A31515"> a+r</span><span style="color:#A31515"> /etc/apt/keyrings/docker.asc</span></span>
<span class="line"><span style="color:#795E26">    echo</span><span style="color:#A31515"> "deb [arch=$(</span><span style="color:#795E26">dpkg</span><span style="color:#0000FF"> --print-architecture</span><span style="color:#A31515">) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(</span><span style="color:#795E26">.</span><span style="color:#A31515"> /etc/os-release &#x26;&#x26; </span><span style="color:#795E26">echo</span><span style="color:#A31515"> "</span><span style="color:#001080">$VERSION_CODENAME</span><span style="color:#A31515">") stable"</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#000000">      > </span><span style="color:#A31515">/etc/apt/sources.list.d/docker.list</span></span>
<span class="line"><span style="color:#795E26">    apt-get</span><span style="color:#A31515"> update</span><span style="color:#0000FF"> -y</span></span>
<span class="line"><span style="color:#795E26">    apt-get</span><span style="color:#A31515"> install</span><span style="color:#0000FF"> -y</span><span style="color:#A31515"> docker-ce</span><span style="color:#A31515"> docker-ce-cli</span><span style="color:#A31515"> containerd.io</span><span style="color:#A31515"> docker-ce-rootless-extras</span></span></code></pre>
<p>The root Docker daemon is disabled and masked, so it cannot be started by the <code>claude</code> user. The <code>docker.sock</code> file is removed, so the <code>claude</code> user cannot connect to the Docker daemon:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    systemctl</span><span style="color:#A31515"> disable</span><span style="color:#0000FF"> --now</span><span style="color:#A31515"> docker.service</span><span style="color:#A31515"> docker.socket</span><span style="color:#A31515"> containerd.service</span><span style="color:#000000"> || </span><span style="color:#795E26">true</span></span>
<span class="line"><span style="color:#795E26">    systemctl</span><span style="color:#A31515"> mask</span><span style="color:#A31515"> docker.service</span><span style="color:#A31515"> docker.socket</span></span>
<span class="line"><span style="color:#795E26">    rm</span><span style="color:#0000FF"> -f</span><span style="color:#A31515"> /run/docker.sock</span></span></code></pre>
<p>Rootless Docker requires a range of subuids and subgids to be assigned to the <code>claude</code> user. We check if the <code>claude</code> user has been assigned a range of subuids and subgids, and if not, we assign the range 165536-231071:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    grep</span><span style="color:#0000FF"> -q</span><span style="color:#A31515"> "^#{AGENT_USER}:"</span><span style="color:#A31515"> /etc/subuid</span><span style="color:#000000"> || </span><span style="color:#795E26">usermod</span><span style="color:#0000FF"> --add-subuids</span><span style="color:#A31515"> 165536-231071</span><span style="color:#008000"> #{AGENT_USER}</span></span>
<span class="line"><span style="color:#795E26">    grep</span><span style="color:#0000FF"> -q</span><span style="color:#A31515"> "^#{AGENT_USER}:"</span><span style="color:#A31515"> /etc/subgid</span><span style="color:#000000"> || </span><span style="color:#795E26">usermod</span><span style="color:#0000FF"> --add-subgids</span><span style="color:#A31515"> 165536-231071</span><span style="color:#008000"> #{AGENT_USER}</span></span></code></pre>
<p>The rootless Docker daemon runs as a <code>systemd --user</code> unit, so the <code>claude</code> user needs a user manager that survives the end of the SSH session that started it. Enabling lingering provides one, keeping the manager running at boot with nobody logged in. It is also what creates the <code>XDG_RUNTIME_DIR</code> holding the session bus that the setup tool in the next step needs. <code>loginctl</code> returns before that directory appears, so we poll for it and fail loudly if it never shows up, rather than letting the next step fail with an unrelated-looking dbus error:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    loginctl</span><span style="color:#A31515"> enable-linger</span><span style="color:#008000"> #{AGENT_USER}</span></span>
<span class="line"><span style="color:#AF00DB">    for</span><span style="color:#001080"> _</span><span style="color:#AF00DB"> in</span><span style="color:#000000"> $(</span><span style="color:#795E26">seq</span><span style="color:#098658"> 1</span><span style="color:#098658"> 30</span><span style="color:#000000">); </span><span style="color:#AF00DB">do</span><span style="color:#000000"> [ -d </span><span style="color:#008000">#{AGENT_RUNTIME_DIR} ] &#x26;&#x26; break; sleep 1; done</span></span>
<span class="line"><span style="color:#000000">    [ -d </span><span style="color:#008000">#{AGENT_RUNTIME_DIR} ] || { echo "XDG_RUNTIME_DIR for #{AGENT_USER} never appeared"; exit 1; }</span></span></code></pre>
<p>Rootless Docker is installed, and the Docker daemon is started as the <code>claude</code> user:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    sudo</span><span style="color:#0000FF"> -u</span><span style="color:#008000"> #{AGENT_USER} -H env \</span></span>
<span class="line"><span style="color:#001080">      XDG_RUNTIME_DIR</span><span style="color:#000000">=#{</span><span style="color:#795E26">AGENT_RUNTIME_DIR}</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      DBUS_SESSION_BUS_ADDRESS=unix:path=#{AGENT_RUNTIME_DIR}/bus</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      PATH=/usr/bin:/usr/sbin:/bin:/sbin</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      dockerd-rootless-setuptool.sh</span><span style="color:#A31515"> install</span></span>
<span class="line"><span style="color:#795E26">    sudo</span><span style="color:#0000FF"> -u</span><span style="color:#008000"> #{AGENT_USER} -H env \</span></span>
<span class="line"><span style="color:#001080">      XDG_RUNTIME_DIR</span><span style="color:#000000">=#{</span><span style="color:#795E26">AGENT_RUNTIME_DIR}</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      DBUS_SESSION_BUS_ADDRESS=unix:path=#{AGENT_RUNTIME_DIR}/bus</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#A31515">      systemctl</span><span style="color:#0000FF"> --user</span><span style="color:#A31515"> enable</span><span style="color:#0000FF"> --now</span><span style="color:#A31515"> docker</span></span></code></pre>
<p>Environment variables are set for the <code>claude</code> user to point to the rootless Docker daemon. This is done by creating a file in <code>/etc/profile.d</code> that sets the <code>XDG_RUNTIME_DIR</code> and <code>DOCKER_HOST</code> environment variables when the <code>claude</code> user logs in:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    cat</span><span style="color:#000000"> > </span><span style="color:#A31515">/etc/profile.d/docker-rootless.sh</span><span style="color:#000000"> &#x3C;&#x3C;</span><span style="color:#000000">'PROFILE'</span></span>
<span class="line"><span style="color:#A31515">if [ "$(id -u)" = "#{AGENT_UID}" ]; then</span></span>
<span class="line"><span style="color:#A31515">  export XDG_RUNTIME_DIR=#{AGENT_RUNTIME_DIR}</span></span>
<span class="line"><span style="color:#A31515">  export DOCKER_HOST=unix://#{AGENT_RUNTIME_DIR}/docker.sock</span></span>
<span class="line"><span style="color:#A31515">fi</span></span>
<span class="line"><span style="color:#000000">PROFILE</span></span>
<span class="line"><span style="color:#795E26">    chown</span><span style="color:#A31515"> root:root</span><span style="color:#A31515"> /etc/profile.d/docker-rootless.sh</span></span>
<span class="line"><span style="color:#795E26">    chmod</span><span style="color:#098658"> 644</span><span style="color:#A31515"> /etc/profile.d/docker-rootless.sh</span></span></code></pre>
<h3 id="installing-nodejs-and-claude-code">Installing Node.js and Claude Code</h3>
<p>Node.js is installed in the sandbox. This is done by adding the NodeSource repository and installing the <code>nodejs</code> package:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    curl</span><span style="color:#0000FF"> -fsSL</span><span style="color:#A31515"> https://deb.nodesource.com/setup_lts.x</span><span style="color:#000000"> | </span><span style="color:#795E26">bash</span><span style="color:#A31515"> -</span></span>
<span class="line"><span style="color:#795E26">    apt-get</span><span style="color:#A31515"> install</span><span style="color:#0000FF"> -y</span><span style="color:#A31515"> nodejs</span></span></code></pre>
<p>Claude Code is installed globally using <code>npm</code>. This allows the <code>claude</code> user to run the <code>claude</code> command from anywhere in the sandbox:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">    npm</span><span style="color:#A31515"> install</span><span style="color:#0000FF"> -g</span><span style="color:#A31515"> @anthropic-ai/claude-code</span></span></code></pre>
<h3 id="rewriting-host-paths-to-guest-paths">Rewriting host paths to guest paths</h3>
<p>The Claude Code configuration copied from the host may point to files in the host’s <code>~/Code</code> directory, which may look like <code>/Users/matthewcasperson/Code</code>. We need to rewrite these paths to point to the sandbox’s <code>/home/claude/Code</code> directory. This is done by reading the <code>.claude.json</code> file and replacing any occurrences of the host path with the guest path:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">  config.vm.provision</span><span style="color:#A31515"> "claude-mcp-paths",</span></span>
<span class="line"><span style="color:#795E26">    type</span><span style="color:#A31515">:</span><span style="color:#A31515"> "shell",</span></span>
<span class="line"><span style="color:#795E26">    run:</span><span style="color:#A31515"> "always",</span></span>
<span class="line"><span style="color:#795E26">    upload_path:</span><span style="color:#A31515"> "/home/vagrant/vagrant-shell",</span></span>
<span class="line"><span style="color:#795E26">    inline:</span><span style="color:#000000"> &#x3C;&#x3C;-</span><span style="color:#000000">SHELL</span></span>
<span class="line"><span style="color:#A31515">    set -euo pipefail</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    command -v jq >/dev/null || { echo "jq is not installed yet; run the main provisioner first"; exit 1; }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    config=#{AGENT_HOME}/.claude.json</span></span>
<span class="line"><span style="color:#A31515">    host_prefix=#{Shellwords.escape("#{HOST_HOME}/Code")}</span></span>
<span class="line"><span style="color:#A31515">    guest_prefix=#{AGENT_HOME}/Code</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    [ -s "</span><span style="color:#001080">$config</span><span style="color:#A31515">" ] || { echo "no </span><span style="color:#001080">$config</span><span style="color:#A31515"> to rewrite"; exit 0; }</span></span>
<span class="line"><span style="color:#A31515">    jq -e . "</span><span style="color:#001080">$config</span><span style="color:#A31515">" >/dev/null 2>&#x26;1 || { echo "</span><span style="color:#001080">$config</span><span style="color:#A31515"> is not valid JSON; leaving it alone"; exit 0; }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    tmp=$(</span><span style="color:#795E26">mktemp</span><span style="color:#A31515"> "</span><span style="color:#001080">$config</span><span style="color:#A31515">.XXXXXX")</span></span>
<span class="line"><span style="color:#A31515">    jq --arg host "</span><span style="color:#001080">$host_prefix</span><span style="color:#A31515">" --arg guest "</span><span style="color:#001080">$guest_prefix</span><span style="color:#A31515">" '</span></span>
<span class="line"><span style="color:#A31515">      def retarget: (. / </span><span style="color:#001080">$host</span><span style="color:#A31515">) | join(</span><span style="color:#001080">$guest</span><span style="color:#A31515">);</span></span>
<span class="line"><span style="color:#A31515">      walk(if type == "string" then retarget else . end)</span></span>
<span class="line"><span style="color:#A31515">      | if (.projects | type) == "object" then</span></span>
<span class="line"><span style="color:#A31515">          .projects = reduce (.projects | to_entries[]) as </span><span style="color:#001080">$e</span><span style="color:#A31515"> ({};</span></span>
<span class="line"><span style="color:#A31515">            .[</span><span style="color:#001080">$e</span><span style="color:#A31515">.key | retarget] = ((.[</span><span style="color:#001080">$e</span><span style="color:#A31515">.key | retarget] // {}) + </span><span style="color:#001080">$e</span><span style="color:#A31515">.value))</span></span>
<span class="line"><span style="color:#A31515">        else . end</span></span>
<span class="line"><span style="color:#A31515">    ' "</span><span style="color:#001080">$config</span><span style="color:#A31515">" > "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    chown #{AGENT_USER}:#{AGENT_USER} "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    chmod 600 "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    mv "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">" "</span><span style="color:#001080">$config</span><span style="color:#A31515">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    echo "rewrote MCP host paths: </span><span style="color:#001080">$host_prefix</span><span style="color:#A31515"> -> </span><span style="color:#001080">$guest_prefix</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">  SHELL</span></span></code></pre>
<h3 id="trusting-the-workspace">Trusting the workspace</h3>
<p>The <code>Code</code> directory is marked as a trusted workspace in the Claude configuration. This allows the AI agent to run without confirmation prompts when accessing files in this directory:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">  config.vm.provision</span><span style="color:#A31515"> "claude-trust",</span></span>
<span class="line"><span style="color:#795E26">    type</span><span style="color:#A31515">:</span><span style="color:#A31515"> "shell",</span></span>
<span class="line"><span style="color:#795E26">    run:</span><span style="color:#A31515"> "always",</span></span>
<span class="line"><span style="color:#795E26">    upload_path:</span><span style="color:#A31515"> "/home/vagrant/vagrant-shell",</span></span>
<span class="line"><span style="color:#795E26">    inline:</span><span style="color:#000000"> &#x3C;&#x3C;-</span><span style="color:#000000">SHELL</span></span>
<span class="line"><span style="color:#A31515">    set -euo pipefail</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    command -v jq >/dev/null || { echo "jq is not installed yet; run the main provisioner first"; exit 1; }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    config=#{AGENT_HOME}/.claude.json</span></span>
<span class="line"><span style="color:#A31515">    [ -s "</span><span style="color:#001080">$config</span><span style="color:#A31515">" ] || install -o #{AGENT_USER} -g #{AGENT_USER} -m 600 /dev/null "</span><span style="color:#001080">$config</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    jq -e . "</span><span style="color:#001080">$config</span><span style="color:#A31515">" >/dev/null 2>&#x26;1 || printf '{}' > "</span><span style="color:#001080">$config</span><span style="color:#A31515">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    tmp=$(</span><span style="color:#795E26">mktemp</span><span style="color:#A31515"> "</span><span style="color:#001080">$config</span><span style="color:#A31515">.XXXXXX")</span></span>
<span class="line"><span style="color:#A31515">    jq '.projects["#{AGENT_HOME}/Code"] =</span></span>
<span class="line"><span style="color:#A31515">          (.projects["#{AGENT_HOME}/Code"] // {}) + {"hasTrustDialogAccepted": true}' </span><span style="color:#EE0000">\</span></span>
<span class="line"><span style="color:#A31515">      "</span><span style="color:#001080">$config</span><span style="color:#A31515">" > "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    chown #{AGENT_USER}:#{AGENT_USER} "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    chmod 600 "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#A31515">    mv "</span><span style="color:#001080">$tmp</span><span style="color:#A31515">" "</span><span style="color:#001080">$config</span><span style="color:#A31515">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    echo "trusted workspace: #{AGENT_HOME}/Code"</span></span>
<span class="line"><span style="color:#A31515">  SHELL</span></span></code></pre>
<h3 id="supporting-bubblewrap-in-apparmor">Supporting Bubblewrap in AppArmor</h3>
<p>An AppArmor profile is created for <code>bwrap</code>, which is the tool used to create sandboxes. This profile allows the <code>claude</code> user to run <code>bwrap</code> without being confined by AppArmor, while still allowing the rest of the system to be protected by AppArmor:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">  config.vm.provision</span><span style="color:#A31515"> "apparmor-bwrap",</span></span>
<span class="line"><span style="color:#795E26">    type</span><span style="color:#A31515">:</span><span style="color:#A31515"> "shell",</span></span>
<span class="line"><span style="color:#795E26">    run:</span><span style="color:#A31515"> "always",</span></span>
<span class="line"><span style="color:#795E26">    upload_path:</span><span style="color:#A31515"> "/home/vagrant/vagrant-shell",</span></span>
<span class="line"><span style="color:#795E26">    inline:</span><span style="color:#000000"> &#x3C;&#x3C;-</span><span style="color:#000000">SHELL</span></span>
<span class="line"><span style="color:#A31515">    set -euo pipefail</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    cat > /etc/apparmor.d/bwrap &#x3C;&#x3C;'PROFILE'</span></span>
<span class="line"><span style="color:#A31515"># This profile allows everything and only exists to give the</span></span>
<span class="line"><span style="color:#A31515"># application a name instead of having the label "unconfined"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">abi &#x3C;abi/4.0>,</span></span>
<span class="line"><span style="color:#A31515">include &#x3C;tunables/global></span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">profile bwrap /usr/bin/bwrap flags=(unconfined) {</span></span>
<span class="line"><span style="color:#A31515">  userns,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">  # Site-specific additions and overrides. See local/README for details.</span></span>
<span class="line"><span style="color:#A31515">  include if exists &#x3C;local/bwrap></span></span>
<span class="line"><span style="color:#A31515">}</span></span>
<span class="line"><span style="color:#A31515">PROFILE</span></span>
<span class="line"><span style="color:#A31515">    chown root:root /etc/apparmor.d/bwrap</span></span>
<span class="line"><span style="color:#A31515">    chmod 644 /etc/apparmor.d/bwrap</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    apparmor_parser -r -W /etc/apparmor.d/bwrap</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A31515">    # Fail provisioning loudly if the sandbox still cannot start, rather than</span></span>
<span class="line"><span style="color:#A31515">    # leaving the agent with a Bash tool that errors on every command. Probed as the</span></span>
<span class="line"><span style="color:#A31515">    # account that will actually run bwrap; this provisioner is ordered after the main</span></span>
<span class="line"><span style="color:#A31515">    # one, which is what creates it.</span></span>
<span class="line"><span style="color:#A31515">    sudo -u #{AGENT_USER} bwrap --ro-bind / / --unshare-net --dev /dev true</span></span>
<span class="line"><span style="color:#A31515">    echo "bwrap sandbox: OK (user namespace + loopback)"</span></span>
<span class="line"><span style="color:#A31515">  SHELL</span></span></code></pre>
<h2 id="building-the-sandbox">Building the sandbox</h2>
<p>Build the sandbox VM with the command:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">vagrant</span><span style="color:#A31515"> up</span></span></code></pre>
<h2 id="executing-the-sandbox">Executing the sandbox</h2>
<p>This is the command to enter the sandbox. The <code>claude-agent</code> script sets the <code>ANTHROPIC_API_KEY</code> environment variable and launches the AI agent as the <code>claude</code> user. The <code>-R 64342:127.0.0.1:64342</code> option forwards the port used by the IntelliJ MCP server from the sandbox to the host machine, so the AI agent can communicate with the IDE. This is because the IntelliJ MCP server only listens on <code>localhost</code> by default, so we need to forward the port to the host machine so the AI agent can communicate with it. The argument <code>--dir MyProject</code> tells the AI agent to start in the <code>MyProject</code> directory (relative to <code>~/Code</code>), which is the root of the project. This is important because the AI agent needs to know where to start looking for files and directories:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">vagrant</span><span style="color:#A31515"> ssh</span><span style="color:#0000FF"> -c</span><span style="color:#A31515"> "sudo /usr/local/sbin/claude-agent --dir MyProject"</span><span style="color:#0000FF"> --</span><span style="color:#0000FF"> -R</span><span style="color:#A31515"> 64342:127.0.0.1:64342</span></span></code></pre>
<p>The IntelliJ MCP server is defined like this in the <code>~/.claude.json</code> configuration file (which is then copied to the sandbox):</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#0451A5">  "intellij"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">    "url"</span><span style="color:#000000">: </span><span style="color:#A31515">"http://127.0.0.1:64342/stream"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "type"</span><span style="color:#000000">: </span><span style="color:#A31515">"http"</span></span>
<span class="line"><span style="color:#000000">  }</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<div class="info"><p>The port is unique on each host, so you will need to replace 64342 with the port used by your IntelliJ MCP server.</p></div>
<h2 id="security-limitations">Security limitations</h2>
<p>While much has been done to lock down the sandbox and prevent Claude from accessing credentials, there are still ways to bypass the restrictions placed on the commands Claude runs.</p>
<p>Consider the following prompt:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a script called </span><span style="color:#800000">`gittest.sh`</span><span style="color:#000000">. Populate it with the commands to create a directory called </span><span style="color:#800000">`/tmp/claude-1001/gittest`</span><span style="color:#000000">, run </span><span style="color:#800000">`git init`</span><span style="color:#000000"> in the directory, touch a file called </span><span style="color:#800000">`test.txt`</span><span style="color:#000000">, and run </span><span style="color:#800000">`git add`</span><span style="color:#000000">. Then run </span><span style="color:#800000">`gittest.sh`</span><span style="color:#000000">.</span></span></code></pre>
<p>Despite the presence of the <code>Bash(git add)</code> and <code>Bash(git commit)</code> deny rules, Claude can still create a new Git repository and add files to it. This is because the deny rules apply only to the <code>git add</code> and <code>git commit</code> commands when run directly, not when run as part of a script.</p>
<p>It is possible to deny file access to <code>.git</code> directories via the Claude sandbox. However, deny rules in the global user settings at <code>~/.claude/settings.json</code> are not relative to the project root. This means any attempts to globally block access to <code>.git</code> files must cover every directory and subdirectory under <code>/home/claude/Code</code>. In my testing, blocking access to <code>.git</code> directories in the global user settings rendered Claude Code unusable with large numbers of directories.</p>
<p>Denying access to directories relative to the current project must be done in project local settings (e.g. <code>~/Code/MyProject/.claude/settings.json</code>). This would remove the performance issues observed attempting to block files globally, but project-level settings are outside the control of this Vagrant sandbox.</p>
<p>It is also worth noting that Docker provides a workaround for both sandbox rules and permissions. Docker runs as a daemon, which means it exists outside the Claude sandbox. Consider the following prompt:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a Dockerfile that installs git. Mount the directory </span><span style="color:#800000">`/home/claude/Code/MyProject`</span><span style="color:#000000"> into the container. Have the container run </span><span style="color:#800000">`touch test.txt`</span><span style="color:#000000"> and </span><span style="color:#800000">`git add`</span><span style="color:#000000"> in the mounted directory.</span></span></code></pre>
<p>This prompt will also allow <code>git add</code> to run, despite the presence of the <code>Bash(git add)</code> deny rule and any <code>.git</code> deny rules in the project’s local settings. This could be used to sneak code into a Git repository or to define Git hooks, which could be disastrous if not picked up during a code review.</p>
<p>Here is another example:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a Dockerfile that echos the contents of the /home/claude/.claude.json file. Mount the /home/claude/.claude.json file into the container. Run the container.</span></span></code></pre>
<p>The <code>/home/claude/.claude.json</code> file potentially contains credentials to support MCP servers. The Claude sandbox explicitly blocks read access to the file to prevent the AI agent from reading the credentials and passing them to a tool like <code>curl</code>. However, Docker is not bound by the Claude sandbox, so it can read the file and exfiltrate the credentials.</p>
<p>These are examples of prioritizing convenience over security, which is a trade-off that must be made when building a sandbox environment.</p>
<p>You could improve the security of the sandbox by simply not installing Docker or denying the ability to execute <code>docker</code> or <code>git</code> commands from prompts. You may also consider explicit instructions in the <code>CLAUDE.md</code> file not to execute Docker in this manner.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The Vagrant sandbox presented in this post provides an isolated environment in which to run the Claude AI agent, providing:</p>
<ul>
<li>No ability for the AI agent to scrape files like <code>/etc/environment</code> to find credentials</li>
<li>No ability to use pre-authenticated CLI tools like <code>aws</code> or <code>azure</code></li>
<li>A disposable operating system that can be destroyed and recreated</li>
<li>Limits on the files that are potentially available to the AI agent</li>
<li>A curated set of tools for the AI agent to use</li>
<li>IDE integration with the IntelliJ MCP server</li>
</ul>
<p>The sandbox does not provide perfect security, though. This was demonstrated with example malicious prompts that can trivially bypass security controls. IDE MCP servers are also powerful and likely offer tools that modify the host machine.</p>
<p>Overall, though, the sandbox strikes a good balance between security and convenience by providing a consistent, limited environment for the AI agent to run in. This sandbox also retains most of the convenience of running agents directly on the host machine, making it a good starting point for anyone looking to run AI agents in a more controlled environment.</p>]]></content>
    </entry>
    <entry>
      <title>Migrating to High Availability - an FSI success story</title>
      <link href="https://octopus.com/blog/migrating-to-high-availability-fsi-success-story" />
      <id>https://octopus.com/blog/migrating-to-high-availability-fsi-success-story</id>
      <published>2026-08-16</published>
      <updated>2026-08-16</updated>
      <summary>How answering every question and breaking a complex upgrade into small, safe steps helped a cautious enterprise finally adopt High Availability.</summary>
      <author>
        <name>Mark Lamprecht, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Some customers move fast. They read the release notes, spin up a test instance, and upgrade the same week. Others move much more carefully, and for good reason. When your deployment pipelines touch thousands of servers across a global business, caution is necessary.</p>
<p>This is the story of one of those customers: a large financial services organization running Octopus Server as the backbone of its deployment process. They’d been on the same version for a long time, well behind current, and running a single Octopus Server instance which was beginning to show signs of stress under the load, so they wanted to move to <a href="https://octopus.com/docs/best-practices/self-hosted-octopus/high-availability">a High Availability (HA) cluster</a>.</p>
<h2 id="a-cautious-multinational-customer">A cautious, multinational customer</h2>
<p>I’ve worked with the Infrastructure team at this multinational FSI long enough to know they are deliberate in their decision making, only making changes to existing structures after they have done extensive planning and testing. Stability is key when so much depends on their infrastructure being available at all times.</p>
<p>The upgrade itself wasn’t just a version bump. It meant also moving from a single server to an HA cluster in a different Active Directory domain; which touched the database, the Octopus home directory and shared storage, authentication, and eventually DNS too. Any one of those pieces going wrong during business hours could affect deployments across their entire estate. Understandably, they wanted to slow down and understand each piece before committing to any of it.</p>
<h2 id="answering-the-questions-one-at-a-time">Answering the questions, one at a time</h2>
<p>My job as their Customer Engineer was to make the unfamiliar feel manageable. That meant drawing on past experience plus my knowledge of both the customer and Octopus, to work through every question they raised, however small: from how the database migration would work, to what moving the Octopus home directory actually involved, to whether existing Tentacles would need any changes, through to how authentication would behave once the HA instance was added to the other Active Directory domain.</p>
<h2 id="small-safe-steps">Small, safe steps</h2>
<p>Answering the customer’s questions built confidence, but confidence alone doesn’t get you through a migration. What actually got this customer moving was a draft plan that broke the upgrade down into small, ordered, and reversible stages, rather than one high-stakes cutover:</p>
<ul>
<li>Notify users of the upcoming upgrade well in advance.</li>
<li>Select the version of Octopus Server they were going to upgrade to.</li>
<li>Set up their load balancer to include two of the five HA nodes as <a href="https://octopus.com/docs/administration/high-availability/auto-scaling-high-availability-nodes#configuring-the-new-node">UI-only nodes</a>.</li>
<li>Besides the <a href="https://octopus.com/docs/administration/upgrading/guide/upgrading-major-releases#rollback-failed-upgrade">Migration Plan</a>, have a clearly defined <a href="https://octopus.com/docs/administration/upgrading/guide/upgrading-major-releases#rollback-failed-upgrade">Roll-back Plan</a>, just in case.</li>
<li>Build the five new HA node Virtual Machines, and add them to the other AD domain well ahead of time.</li>
<li>On the day, put the existing Octopus server into <a href="https://octopus.com/docs/administration/managing-infrastructure/maintenance-mode">maintenance mode</a> and then <a href="https://octopus.com/docs/administration/managing-infrastructure/maintenance-mode">let running tasks drain</a>, so nothing is mid-flight when the migration starts.</li>
<li><a href="https://octopus.com/docs/administration/upgrading/guide/upgrading-major-releases#backup-the-sql-server-database">Back up</a> the database and restore it into the new environment, rather than pointing the new environment straight at the live database - which also means rolling back was safe and easy if it came to it.</li>
<li>Move <a href="https://octopus.com/docs/best-practices/self-hosted-octopus/high-availability#file-storage">shared storage</a> across separately, and confirm it before moving on.</li>
<li>Install Octopus Server on a single High Availability node first, then:
<ul>
<li>Test authentication with a couple of <a href="https://octopus.com/docs/security/authentication#local-authentication">local Octopus users</a> before touching anything related to sign-in for their teams.</li>
<li>Perform a pre-selected list of test deployments.</li>
</ul>
</li>
<li>Now install Octopus Server on the other HA nodes, pausing to check local Octopus login access and to do test deployments after each one.</li>
<li>Once the basics are proven to work, only then enable <a href="https://octopus.com/docs/security/authentication/active-directory">Active Directory</a>.</li>
<li>Last of all, point DNS at the load balancer’s IP address for the HA cluster once everything else is verified. That way, nobody can accidentally start using the new Octopus Deploy instance before it is actually ready.</li>
</ul>
<p>Each stage had a clear checkpoint before moving to the next. If something looked wrong at any stage then they could pause or roll back without having already committed to the whole change. For an Infrastructure team whose instinct was to slow down and check everything, that mattered more than any amount of reassurance could on its own.</p>
<h2 id="the-outcome">The outcome</h2>
<p>The customer completed their move to an HA cluster, and it gave them a foundation to build on. Instead of staying several versions behind, they settled into a regular 6-monthly upgrade cadence, staying close enough to current that they didn’t feel like they were on the bleeding edge. Years later, they’re still on that cadence, still running a large and complex Octopus environment, and rarely need to raise a support ticket, which is its own kind of compliment. As their team lead said:</p>
<p>“…<em>we could not have done our Octopus instance migration without you; and we don’t know of any company that would provide the level of services you have - your help saved us a lot of money!</em>”</p>
<h2 id="what-this-means-for-other-cautious-teams">What this means for other cautious teams</h2>
<p>If you’re a customer who’s stuck on an old version because the upgrade feels too big to risk, the fix is to get solid answers to your questions that are grounded in your needs and clear in what is required of you. So answer the difficult questions and break the upgrade down into stages small enough that each one is genuinely low-risk on its own. After all, hesitation is often just a request for a plan that hasn’t been written yet.</p>
<p>So if you’re planning a similar move, start with our <a href="https://octopus.com/docs/administration/high-availability/design">Designing Octopus for High Availability</a> and our <a href="https://octopus.com/docs/administration/upgrading/guide">Upgrade</a> guides; and if you need more help, then by all means get in touch with your account team here at Octopus.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Policies</title>
      <link href="https://octopus.com/blog/octo-easy-mode-22-policy" />
      <id>https://octopus.com/blog/octo-easy-mode-22-policy</id>
      <published>2026-08-15</published>
      <updated>2026-08-15</updated>
      <summary>Learn how to enforce policies with Platform Hub</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Governance, Risk, and Compliance (GRC) is a crucial requirement in regulated industries. This is especially true with the rise of AI, as this <a href="https://www.apra.gov.au/news-and-publications/apra-letter-industry-artificial-intelligence-ai">APRA Letter to Industry on Artificial Intelligence</a> demonstrates. Since deployments are where changes meet production systems, it is important to be able to define and enforce policies ensuring Octopus projects meet an organization’s requirements.</p>
<p>Platform Hub provides the ability to define Open Policy Agent (OPA) policies, written in <a href="https://www.openpolicyagent.org/docs/policy-language">Rego</a>, that can be applied to Octopus projects. In this post, you’ll create a policy that enforces the presence of the Self-Support process template created in the <a href="/blog/octo-easy-mode-21-self-support">previous post</a>.</p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>An <a href="https://octopus.com/start">Octopus Cloud</a> account. If you don’t have one, you can sign up for a free trial.</li>
<li>The Octopus AI Assistant Chrome extension. You can install it from the <a href="https://chromewebstore.google.com/detail/octopus-ai-assistant/agfpjjibnieiihjoehophlbamcifdfha">Chrome Web Store</a>.</li>
<li>A <a href="https://platform.claude.com/docs/en/get-api-key">Claude API key</a>.</li>
<li>A <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">GitHub personal access token</a>.</li>
<li>An <a href="https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key">Octopus Agent API key</a>.</li>
</ul>
<div class="hint"><p>The Octopus AI Assistant will work with an on-premises Octopus instance, but it requires more configuration. The
cloud-hosted version of Octopus doesn’t need extra configuration. This means the cloud-hosted version is the easiest way to get started.</p></div>
<h2 id="creating-the-project">Creating the project</h2>
<p>Use the instructions from the <a href="/blog/octo-easy-mode-21-self-support">previous post</a> to create the Web App project and then add the Self-Support process template to the project.</p>
<p>In this scenario, you’ll define a process to enforce a policy requiring the Self-Support process template to be present in all projects. This will be an incremental process that first identifies noncompliant projects and then begins enforcing the policy.</p>
<h2 id="creating-the-policy">Creating the policy</h2>
<p>The mock Git repository linked as part of the previous post contains a policy that enforces the presence of the Self-Support process template:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/policy.png"><img src="/blog/i/x/octo-easy-mode-22-policy/policy.png" alt="Self-Support policy" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/policy.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/policy.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/policy.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>Policies have two parts:</p>
<ol>
<li>The Rego that defines which projects the policy applies to (Scope Rego).</li>
<li>The Rego that defines the policy itself (Conditions Rego).</li>
</ol>
<p>The default Scope Rego is this:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>package self_support_exists</span></span>
<span class="line"><span></span></span>
<span class="line"><span>default evaluate := true</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The following are examples of available scoping options:</span></span>
<span class="line"><span>evaluate if {</span></span>
<span class="line"><span>    # Scope evaluation by Environment name</span></span>
<span class="line"><span>    # input.Environment.Name == "&#x3C;environment-name>"</span></span>
<span class="line"><span>    # Scope evaluation to Space Id</span></span>
<span class="line"><span>    # input.Space.Id == "Spaces-1"</span></span>
<span class="line"><span>    # Scope evaluation to multiple Spaces</span></span>
<span class="line"><span>    # input.Project.Slug in ["&#x3C;project-slug>", "&#x3C;project-slug2>"]</span></span>
<span class="line"><span>}</span></span></code></pre>
<p>The commented conditions need to be updated to reflect your local environment. The most common change is to define the space that the policy applies to. In this example, the policy applies to the custom space with the ID <code>Spaces-1234</code>:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>package self_support_exists</span></span>
<span class="line"><span></span></span>
<span class="line"><span>default evaluate := true</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The following are examples of available scoping options:</span></span>
<span class="line"><span>evaluate if {</span></span>
<span class="line"><span>    # Scope evaluation by Environment name</span></span>
<span class="line"><span>    # input.Environment.Name == "&#x3C;environment-name>"</span></span>
<span class="line"><span>    # Scope evaluation to Space Id</span></span>
<span class="line"><span>    input.Space.Id == "Spaces-1234"</span></span>
<span class="line"><span>    # Scope evaluation to multiple Spaces</span></span>
<span class="line"><span>    # input.Project.Slug in ["&#x3C;project-slug>", "&#x3C;project-slug2>"]</span></span>
<span class="line"><span>}</span></span></code></pre>
<div class="warning"><p>Changes committed to the mock Git repo are reset after a period of time. It is expected that the changes to the policy will be reverted.</p></div>
<p>The sample Conditions Rego is this:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>package self_support_exists</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Default: Deny all deployments</span></span>
<span class="line"><span>default result := {"allowed": false}</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Allow: If a specific Process Template is used and not bypassed</span></span>
<span class="line"><span>result := {"allowed": true} if {</span></span>
<span class="line"><span>    some step in input.Steps</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    # Ensure the step is derived from a Process Template</span></span>
<span class="line"><span>    step.Source.Type == "Process Template"</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    # Target a specific template by its unique slug or ID</span></span>
<span class="line"><span>    step.Source.SlugOrId == "self-support"</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    # Verify this specific step hasn't been added to the skipped list</span></span>
<span class="line"><span>    not step.Id in input.SkippedSteps</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    # Verify the step is enabled</span></span>
<span class="line"><span>    step.Enabled == true</span></span>
<span class="line"><span>}</span></span></code></pre>
<p>This policy ensures that a step of type <code>Process Template</code> is present in the deployment process and linked to the Self-Support process template, as indicated by the slug <code>self-support</code>. The step must not be skipped and must be enabled; otherwise, the deployment will fail.</p>
<p>The easiest way to see the step types is to download the deployment process as JSON:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/download-as-json.png"><img src="/blog/i/x/octo-easy-mode-22-policy/download-as-json.png" alt="Deployment process JSON" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/download-as-json.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/download-as-json.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/download-as-json.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>The resulting JSON blob can be quite large, but towards the end of the file, you will see code that looks like this:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#A31515">"Actions"</span><span style="color:#000000">: [</span></span>
<span class="line"><span style="color:#000000">        {</span></span>
<span class="line"><span style="color:#0451A5">          "Id"</span><span style="color:#000000">: </span><span style="color:#A31515">"fc95fd56-778c-42c6-ae35-fe611dfb4619"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "Name"</span><span style="color:#000000">: </span><span style="color:#A31515">"Run a Process Template"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "Slug"</span><span style="color:#000000">: </span><span style="color:#A31515">"run-a-process-template"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "ActionType"</span><span style="color:#000000">: </span><span style="color:#A31515">"Octopus.ProcessTemplate"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "Notes"</span><span style="color:#000000">: </span><span style="color:#0000FF">null</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "IsDisabled"</span><span style="color:#000000">: </span><span style="color:#0000FF">false</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "CanBeUsedForProjectVersioning"</span><span style="color:#000000">: </span><span style="color:#0000FF">false</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "IsRequired"</span><span style="color:#000000">: </span><span style="color:#0000FF">false</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "WorkerPoolId"</span><span style="color:#000000">: </span><span style="color:#0000FF">null</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">          "Container"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">            "Image"</span><span style="color:#000000">: </span><span style="color:#0000FF">null</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "FeedId"</span><span style="color:#000000">: </span><span style="color:#0000FF">null</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "GitUrl"</span><span style="color:#000000">: </span><span style="color:#0000FF">null</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "Dockerfile"</span><span style="color:#000000">: </span><span style="color:#0000FF">null</span></span>
<span class="line"><span style="color:#000000">          },</span></span></code></pre>
<p>The <code>ActionType</code> property indicates the type of the step. In this example we can see this is a process template step. This value directly relates to the <code>step.ActionType</code> property in the <a href="https://octopus.com/docs/platform-hub/policies/schema#steps">rego schema</a>.</p>
<p>In this case, however, the <code>step.Source.Type == "Process Template"</code> condition is defined to indicate that a process template must exist. This implies that the step must have an <code>ActionType</code> of <code>Octopus.ProcessTemplate</code>. The <a href="https://octopus.com/docs/platform-hub/policies/schema#source-object">documentation</a> provides the exact values for the <code>Source.Type</code> property.</p>
<p>You’ll then see a section that looks like this:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#A31515">"Properties"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">            "SelfSupport.WorkerPool"</span><span style="color:#000000">: </span><span style="color:#A31515">"WorkerPools-7706"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "SelfSupport.Claude.ApiKey"</span><span style="color:#000000">: </span><span style="color:#A31515">"#{LibraryVariableSet.Claude.ApiKey}"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "SelfSupport.Octopus.ApiKey"</span><span style="color:#000000">: </span><span style="color:#A31515">"#{LibraryVariableSet.Octopus.ApiKey}"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "SelfSupport.GitHub.PAT"</span><span style="color:#000000">: </span><span style="color:#A31515">"#{LibraryVariableSet.GitHub.PAT}"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "SelfSupport.RunCondition"</span><span style="color:#000000">: </span><span style="color:#A31515">"#{if Octopus.Deployment.Error}True#{/if}"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "Octopus.Action.ProcessTemplate.Reference.Slug"</span><span style="color:#000000">: </span><span style="color:#A31515">"self-support"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "Octopus.Action.ProcessTemplate.Reference.VersionMask"</span><span style="color:#000000">: </span><span style="color:#A31515">"4.X"</span></span>
<span class="line"><span style="color:#000000">          },</span></span></code></pre>
<p>The <code>Octopus.Action.ProcessTemplate.Reference.Slug</code> property indicates the slug of the process template that the step is associated with, and this is the value that is assigned to the <code>step.Source.SlugOrId</code> property in the Conditions Rego.</p>
<h2 id="publishing-a-policy">Publishing a policy</h2>
<p>The policy has the <code>Violation Action</code> setting configured to <code>Warning</code>. This means that if the scope is met but the conditions are not, a warning will be added to the audit log. Leave this value as it is for now.</p>
<p>The policy must be published before it can be evaluated:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/publish-policy.png"><img src="/blog/i/x/octo-easy-mode-22-policy/publish-policy.png" alt="Publish button" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/publish-policy.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/publish-policy.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/publish-policy.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>You will be asked to specify the policy version (or be forced to use version 1.0.0 if you are publishing it for the first time).</p>
<p>You can then configure the policy to be active or inactive.</p>
<p>An active policy will add a warning to the audit log, or fail the deployment (depending on the violation action), for any deployments that do not meet the policy.</p>
<p>An inactive policy is not considered when evaluating deployments. You can publish inactive policies to evaluate them against previous deployments without impacting any future deployments.</p>
<div class="hint"><p>The published policy remains in effect even if the Git repo is reset.</p></div>
<h2 id="discovering-non-compliant-deployments">Discovering non-compliant deployments</h2>
<p>The easiest way to fail the policy is to disable the Self-Support process template:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/disable-step.png"><img src="/blog/i/x/octo-easy-mode-22-policy/disable-step.png" alt="Disable step" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/disable-step.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/disable-step.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/disable-step.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>Create a release and deploy it. The deployment will succeed, but the audit log will show a warning that the policy was not met. You can filter the audit log by the event category <code>Compliance Policy evaluated as non-compliant with warning outcome</code>:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/audit-log-warning.png"><img src="/blog/i/x/octo-easy-mode-22-policy/audit-log-warning.png" alt="Audit log warning" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/audit-log-warning.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/audit-log-warning.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/audit-log-warning.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>You can review these audit log entries to identify which projects need to have the Self-Support process template added to their deployment process. Crucially, you have not disrupted any deployments by setting the <code>Violation Action</code> to <code>Warning</code>.</p>
<h2 id="blocking-deployments">Blocking deployments</h2>
<p>Once you are satisfied that all projects have the Self-Support process template added to their deployment process, you can set the policy to block deployments. This will fail any project deployments that do not meet the policy.</p>
<p>Return to the <code>Policies</code> screen, edit the <code>Self-Support exists</code> policy, and change the <code>Violation Action</code> to <code>Block</code>:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/block-deployments.png"><img src="/blog/i/x/octo-easy-mode-22-policy/block-deployments.png" alt="Block deployments" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/block-deployments.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/block-deployments.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/block-deployments.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<div class="warning"><p>The mock Git repository has likely reset itself at this point, so you will need to reapply any changes to the scope rego.</p></div>
<p>Commit the changes and publish a new version of the policy in active mode.</p>
<p>Now, when you deploy a new release, it will be blocked by the policy and fail:</p>
<p><a href="/blog/img/octo-easy-mode-22-policy/deployment-blocked.png"><img src="/blog/i/x/octo-easy-mode-22-policy/deployment-blocked.png" alt="Deployment blocked" loading="lazy" srcset="/blog/i/600/octo-easy-mode-22-policy/deployment-blocked.webp 600w, /blog/i/1000/octo-easy-mode-22-policy/deployment-blocked.webp 1000w, /blog/i/2000/octo-easy-mode-22-policy/deployment-blocked.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<h2 id="what-just-happened">What just happened</h2>
<p>You created a policy to ensure the Self-Support process template is present in all projects. You then published the policy in warning mode to identify non-compliant deployments without impacting them. Finally, you changed the policy to block mode to prevent future deployments that do not meet it.</p>
<h2 id="next-steps">Next steps</h2>
<p>To productionize the example, you can be notified directly when a policy is violated with <a href="https://octopus.com/docs/administration/managing-infrastructure/subscriptions">subscriptions</a>. A subscription responds to specific audit log events and then calls an external system like email, Slack, or an HTTP webhook.</p>
<p>You will also need to copy the policy to your own Git repo. Do this by saving the file <a href="https://mockgit.octopusdemos.com/browse/usernamegoeshere/platformhubrepo/.octopus/policies/self_support_exists.ocl?branch=main">self-support-exists.ocl</a>, committing it to your own Git repo, and configuring the <a href="https://octopus.com/docs/platform-hub#git-credentials-in-platform-hub">Platform Hub version control settings</a>.</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Kubernetes Microservice Orchestration</title>
      <link href="https://octopus.com/blog/octo-easy-mode-20-microservices" />
      <id>https://octopus.com/blog/octo-easy-mode-20-microservices</id>
      <published>2026-08-14</published>
      <updated>2026-08-14</updated>
      <summary>Learn how to orchestrate the deployment of multiple Kubernetes microservices</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>As applications grow in complexity, they may be split into multiple independent microservices, each with its own deployment pipeline.</p>
<p>There are many characteristics defining microservices, but from a deployment perspective, <a href="https://martinfowler.com/articles/microservices.html">Martin Fowler notes that</a>:</p>
<blockquote>
<p>These services are built around business capabilities and are independently deployable by fully automated deployment machinery.</p>
</blockquote>
<p>Independently deployable microservices don’t inherently need any special orchestration as projects in Octopus are already independently deployable. However, it is often useful to be able to promote a set of microservices as a single unit between environments, for example, from Test to Production. And while microservices are ideally independent, in practice, they often require a particular deployment order.</p>
<p>To support these scenarios, Octopus provides the <a href="https://octopus.com/docs/projects/coordinating-multiple-projects/deploy-release-step">Deploy a Release</a> step, which allows one project to trigger the deployment of another project. This allows you to create an orchestration project that coordinates the deployment of multiple microservices.</p>
<p>In the <a href="/blog/octo-easy-mode-19-bluegreen">previous post</a>, you created a project that demonstrates blue/green deployments.</p>
<p>In this post, you’ll create an orchestration project that coordinates the sequential deployment of multiple Kubernetes microservices.</p>
<p><a href="https://samples.octopus.app/app#/Spaces-1213/projects/kubernetes-microservice-orchestration">Live Demo</a></p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>An <a href="https://octopus.com/start">Octopus Cloud</a> account. If you don’t have one, you can sign up for a free trial.</li>
<li>The Octopus AI Assistant Chrome extension. You can install it from the <a href="https://chromewebstore.google.com/detail/octopus-ai-assistant/agfpjjibnieiihjoehophlbamcifdfha">Chrome Web Store</a>.</li>
</ul>
<div class="hint"><p>The Octopus AI Assistant will work with an on-premises Octopus instance, but it requires more configuration. The
cloud-hosted version of Octopus doesn’t need extra configuration. This means the cloud-hosted version is the easiest way to get started.</p></div>
<h2 id="creating-the-project">Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Create a token account called "Mock Token".</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Create a feed called "Docker Hub" pointing to "https://index.docker.io" using anonymous authentication.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Add a target called "Mock K8s", with the tag "Kubernetes", using the token account, pointing to "https://mockk8s.octopusdemos.com", using the health check image "octopusdeploy/worker-tools:6.5.0-ubuntu.22.04" from the "Docker Hub" feed, using the worker pool "Hosted Ubuntu".</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create a Kubernetes project called "20. Microservice 1", and then:</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Place the project in the "Orchestrator" project group.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Configure the Kubernetes steps to use client side apply (client side apply is required by the "Mock K8s" target).</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Disable verification checks in the Kubernetes steps (verification checks are not supported by the "Mock K8s" target).</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Enable retries on the Kubernetes step.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create a Kubernetes project called "20. Microservice 2", and then:</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Place the project in the "Orchestrator" project group.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Configure the Kubernetes steps to use client side apply (client side apply is required by the "Mock K8s" target).</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Disable verification checks in the Kubernetes steps (verification checks are not supported by the "Mock K8s" target).</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Enable retries on the Kubernetes step.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create an Orchestration project called "20. Kubernetes Microservice Orchestration" managing the projects "20. Microservice 1" and "20. Microservice 2".</span></span></code></pre>
<div class="hint"><p>The document separator (<code>---</code>) is used to split the prompt into multiple sections. Each section is applied sequentially, which allows you to create different types of resources in a single prompt.</p></div>
<p>The first section creates the shared infrastructure: a token account, a Docker Hub feed, and a <a href="https://octopus.com/docs/kubernetes/targets/kubernetes-api">Kubernetes target</a> pointing to a mock Kubernetes server. The mock server exposes just enough of the Kubernetes API to allow deployment steps to execute successfully, without requiring access to a real Kubernetes cluster. See <a href="/blog/octo-easy-mode-14-k8s">Octopus Easy Mode - Kubernetes</a> for more details on the mock Kubernetes server.</p>
<p>The second and third sections each create identical Kubernetes microservice projects.</p>
<p>The fourth section creates the orchestration project, which is the focus of this post.</p>
<h2 id="the-orchestration-project">The orchestration project</h2>
<p>The <code>Kubernetes Microservice Orchestration</code> project uses the <code>Deploy a Release</code> step type to trigger deployments of each child project. Its deployment process contains two sequential steps:</p>
<ul>
<li><strong>Deploy K8s Microservice 1</strong> — deploys a release of <code>K8s Microservice 1</code></li>
<li><strong>Deploy K8s Microservice 2</strong> — deploys a release of <code>K8s Microservice 2</code></li>
</ul>
<p>The child projects are deployed sequentially: <code>K8s Microservice 2</code> only begins once <code>K8s Microservice 1</code> has completed successfully. This ordering ensures that any service dependencies are respected.</p>
<p>Or, if you prefer, both child projects could be deployed in parallel by configuring the step <a href="https://octopus.com/docs/projects/steps/conditions#start-trigger">start trigger</a>:</p>
<p><a href="/blog/img/octo-easy-mode-20-microservices/step-start-trigger.png"><img src="/blog/i/x/octo-easy-mode-20-microservices/step-start-trigger.png" alt="Deploy a release step start trigger" loading="lazy" srcset="/blog/i/600/octo-easy-mode-20-microservices/step-start-trigger.webp 600w, /blog/i/1000/octo-easy-mode-20-microservices/step-start-trigger.webp 1000w, /blog/i/2000/octo-easy-mode-20-microservices/step-start-trigger.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>The orchestration project uses a lifecycle promoting releases through <code>Development</code>, <code>Test</code>, and <code>Production</code> environments in sequence. When a deployment is triggered for an environment, both child projects are deployed to that same environment.</p>
<h2 id="the-deployment-process">The deployment process</h2>
<p>The first step is to create a release of the child projects. There is nothing special about the release creation process for the child projects.</p>
<p>Once a release is available for each child project, a release of the orchestration project can be created. The projects referenced by the <code>Deploy a Release</code> steps will be presented much like a package reference, allowing you to select the release of each child project to deploy:</p>
<p><a href="/blog/img/octo-easy-mode-20-microservices/deploy-a-release-project-versions.png"><img src="/blog/i/x/octo-easy-mode-20-microservices/deploy-a-release-project-versions.png" alt="Deploy a release package selection" loading="lazy" srcset="/blog/i/600/octo-easy-mode-20-microservices/deploy-a-release-project-versions.webp 600w, /blog/i/1000/octo-easy-mode-20-microservices/deploy-a-release-project-versions.webp 1000w, /blog/i/2000/octo-easy-mode-20-microservices/deploy-a-release-project-versions.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>Deploying the orchestration project release triggers the deployment of each child project in sequence to the same environment. In this way, the orchestration project serves as a single deployment unit for the set of microservices.</p>
<p>Importantly, it is still possible to deploy a release of each child project independently, without using the orchestration project. The <code>Deploy a Release</code> step can skip the deployment of a child project if it has already been deployed to the target environment, or it can redeploy the child project. This allows the child projects to retain their independence while still allowing them to be promoted together as a single unit when required.</p>
<h2 id="what-just-happened">What just happened?</h2>
<p>You created a sample setup consisting of:</p>
<ul>
<li>Two child Kubernetes microservice projects (<code>K8s Microservice 1</code> and <code>K8s Microservice 2</code>)</li>
<li>A parent orchestration project (<code>Kubernetes Microservice Orchestration</code>) that uses <code>Deploy a Release</code> steps to coordinate the sequential deployment of both microservices</li>
</ul>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Self Support</title>
      <link href="https://octopus.com/blog/octo-easy-mode-21-self-support" />
      <id>https://octopus.com/blog/octo-easy-mode-21-self-support</id>
      <published>2026-08-14</published>
      <updated>2026-08-14</updated>
      <summary>Learn how to enable self-support features in Octopus</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Done well, Platform Engineering can drive significant value for organizations. But it is easy to fall prey to the temptation to simply shift responsibility to the platform team. <a href="https://jellyfish.co/library/platform-engineering/anti-patterns/">Rebranding the Operations Team</a> is an example of this anti-pattern:</p>
<blockquote>
<p>Organizations realize they need platform engineering, so they simply rename their existing Infrastructure or Operations team to Platform Engineers without changing the underlying operating model. The team continues to work in a ticket-based, reactive mode, handling manual requests for environments and access.</p>
</blockquote>
<p>This is a common scenario, with the <a href="https://platformengineering.org/blog/announcing-the-state-of-platform-engineering-vol-4">State of Platform Engineering Volume 4 2025</a> report noting that:</p>
<blockquote>
<p>The distribution of staff and funding models shows that most organizations are still in the early stages of platform maturity. The largest share, at 45.5%, have a dedicated, budgeted team that remains mostly reactive, which suggests that platform functions are established but not yet strategic.</p>
</blockquote>
<p><img src="/blog/i/x/octo-easy-mode-21-self-support/platform-engineering-investment.png" alt="Platform Engineering Investment" srcset="/blog/i/600/octo-easy-mode-21-self-support/platform-engineering-investment.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/platform-engineering-investment.webp 1000w, /blog/i/2000/octo-easy-mode-21-self-support/platform-engineering-investment.webp 1988w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1988" height="1134"></p>
<p>Documenting common solutions to common problems and surfacing that information on demand is one strategy Platform Engineering teams can implement to reduce their support burden. In this post, you’ll create a sample project guaranteed to fail and then add a step using AI to provide a self-support solution.</p>
<p><a href="https://samples.octopus.app/app#/Spaces-1213/projects/self-support">Live Demo</a></p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>An <a href="https://octopus.com/start">Octopus Cloud</a> account. If you don’t have one, you can sign up for a free trial.</li>
<li>The Octopus AI Assistant Chrome extension. You can install it from the <a href="https://chromewebstore.google.com/detail/octopus-ai-assistant/agfpjjibnieiihjoehophlbamcifdfha">Chrome Web Store</a>.</li>
<li>A <a href="https://platform.claude.com/docs/en/get-api-key">Claude API key</a>.</li>
<li>A <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">GitHub personal access token</a>.</li>
<li>An <a href="https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key">Octopus Agent API key</a>.</li>
</ul>
<div class="hint"><p>The Octopus AI Assistant will work with an on-premises Octopus instance, but it requires more configuration. The
cloud-hosted version of Octopus doesn’t need extra configuration. This means the cloud-hosted version is the easiest way to get started.</p></div>
<h2 id="creating-the-project">Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a Kubernetes project called "Web App", and then:</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Add a step called </span><span style="color:#800000">`Check for web-app-secret`</span><span style="color:#000000"> before the existing YAML deployment step that checks for the presence of a Kubernetes secret named "web-app-secret" using a "Run a kubectl script" step.</span></span>
<span class="line"><span style="color:#0451A5">    *</span><span style="color:#000000"> Retry the check inside a bash loop 30 times. This allows the script to check every replica of the mock Kubernetes cluster for the secret. Do not sleep as part of the loop.</span></span>
<span class="line"><span style="color:#0451A5">    *</span><span style="color:#000000"> The step must run against the "Kubernetes" target tag.</span></span>
<span class="line"><span style="color:#0451A5">    *</span><span style="color:#000000"> Use the namespace "#{Octopus.Environment.Name | ToLower | Replace "[</span><span style="color:#A31515">^A-Za-z0-9</span><span style="color:#000000">]" -}#{if Octopus.Deployment.Tenant.Name}#{Octopus.Deployment.Tenant.Name | ToLower | Replace "[</span><span style="color:#A31515">^A-Za-z0-9</span><span style="color:#000000">]" -}#{/if}".</span></span>
<span class="line"><span style="color:#0451A5">    *</span><span style="color:#000000"> Uses the "Hosted Ubuntu" worker pool</span></span>
<span class="line"><span style="color:#0451A5">    *</span><span style="color:#000000"> The step runs the "octopusdeploy/worker-tools:6.5.0-ubuntu.22.04" execution container from the "Docker Hub" feed with anonymous authentication.</span></span>
<span class="line"><span style="color:#0451A5">    *</span><span style="color:#000000"> If the secret does not exist, exit with an error.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The project must require a tenant to run.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Create 3 tenants named "APAC", "EMEA", and "AMER" that can run the project.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Create a kubernetes target and scope it to the tenants.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Create a runbook called "Create Secret" in the project "Web App" with the description "Creates the secret required by the Web App deployment."</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The runbook must include a step to create the secret called "web-app-secret" by deploying a secret YAML resource. Include a dummy value for the data.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The step runs the "octopusdeploy/worker-tools:6.5.0-ubuntu.22.04" execution container from the "Docker Hub" feed with anonymous authentication.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The step uses the "Hosted Ubuntu" worker pool.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Disable server side apply.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Disable step verification.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The step runs against the "Kubernetes" target tag.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The step runs in the namespace "#{Octopus.Environment.Name | ToLower | Replace "[</span><span style="color:#A31515">^A-Za-z0-9</span><span style="color:#000000">]" -}#{if Octopus.Deployment.Tenant.Name}#{Octopus.Deployment.Tenant.Name | ToLower | Replace "[</span><span style="color:#A31515">^A-Za-z0-9</span><span style="color:#000000">]" -}#{/if}".</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Enable retries on the step.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The runbook must require a tenant to run.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> The runbook is scoped to the "Development", "Test", and "Production" environments.</span></span></code></pre>
<p>There are some important details to note about this prompt.</p>
<p>It creates a mock Kubernetes deployment target. This target references a web application exposing just enough of the Kubernetes API to allow deployment steps to execute successfully, without requiring access to a real Kubernetes cluster. See <a href="/blog/octo-easy-mode-14-k8s">Octopus Easy Mode - Kubernetes</a> for more details on the mock Kubernetes server.</p>
<p>The mock Kubernetes server is hosted as an Azure Container App and can scale out to multiple replicas. But the web server is isolated and does not share state between replicas. This means that if a secret is created on one replica, it will not be visible to other replicas. This is why the deployment step checks for the secret up to 30 times, allowing it to verify every replica of the mock Kubernetes cluster.</p>
<p>You would not do this with a real Kubernetes cluster – it is only required to support the mock Kubernetes server.</p>
<h2 id="failing-the-deployment">Failing the deployment</h2>
<p>Create a release of the <code>Web App</code> project and deploy it to the <code>Development</code> environment. The deployment will fail because the required secret does not exist. This is expected behavior, and it simulates a common scenario where a deployment fails due to a missing prerequisite.</p>
<p>You know that there is a supporting runbook that can create the secret. However, individual members of a DevOps team may not be aware that the runbook exists or that they have the permissions to run it. This will almost certainly result in a support request to the Platform Engineering team, which is exactly what you want to avoid.</p>
<p>To enable self-support, you will configure Platform Hub and link a process template that queries a knowledge base to the deployment process.</p>
<h2 id="configuring-platform-hub">Configuring Platform Hub</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Configure the Platform Hub git repo to point to https://mockgit.octopusdemos.com/repo/platformhubrepo using the ".octopus" base path.</span></span></code></pre>
<p>This prompt configures Platform Hub to point to a mock Git repository. The mock repository contains several example process templates ready to add to your deployment process.</p>
<p><a href="/blog/img/octo-easy-mode-21-self-support/platfrm-hub-version-control.png"><img src="/blog/i/x/octo-easy-mode-21-self-support/platfrm-hub-version-control.png" alt="Platform Hub version control" loading="lazy" srcset="/blog/i/600/octo-easy-mode-21-self-support/platfrm-hub-version-control.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/platfrm-hub-version-control.webp 1000w, /blog/i/2000/octo-easy-mode-21-self-support/platfrm-hub-version-control.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>You can browse the mock repository at the URL <code>https://mockgit.octopusdemos.com/browse/&#x3C;username>/platformhubrepo</code>, replacing <code>&#x3C;username></code> with the GUID defined in the Platform Hub authentication section.</p>
<div class="warning"><p>The mock git repo resets after a period of time, so any changes you make will be lost. This is expected behavior.</p></div>
<p>You will add the process template called <code>Self Support</code> to the deployment process:</p>
<p><a href="/blog/img/octo-easy-mode-21-self-support/self-support-process-template.png"><img src="/blog/i/x/octo-easy-mode-21-self-support/self-support-process-template.png" alt="Process template" loading="lazy" srcset="/blog/i/600/octo-easy-mode-21-self-support/self-support-process-template.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/self-support-process-template.webp 1000w, /blog/i/2000/octo-easy-mode-21-self-support/self-support-process-template.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>The <code>Self Support</code> process template contains a <code>Run Claude Agent</code> step that provides feedback regarding a failed deployment. This is the prompt it runs:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">The deployment #{Octopus.Deployment.Id} in space #{Octopus.Space.Name} for project #{Octopus.Project.Name} has failed.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Your task is to provide guidance on how to troubleshoot and resolve the issue.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#000000">Provide a solution to the failure by:</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Getting the Octopus deployment logs and determining which step failed</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Getting the configuration of the deployment steps</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Getting any git diffs from the commits noted in the build information</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> If the project is configured with config-as-code, checking the git repo hosting the project configuration for any recent changes</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> Checking the GitHub issues at https://github.com/OctopusSolutionsEngineering/SelfSupportDemo for any relevant issues or known bugs that match the failed step</span></span>
<span class="line"></span>
<span class="line"><span style="color:#800000;font-weight:bold"># Notes</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> You must only use the MCP servers to gather information.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> You must not run any scripts, execute curl, or run any other CLI tools to gather information.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> You must not list every issue you find in GitHub.</span></span>
<span class="line"><span style="color:#0451A5">*</span><span style="color:#000000"> You must only reference an issue if it is directly related to the current deployment failure.</span></span></code></pre>
<p>Two MCP servers, Octopus and GitHub, are configured to provide additional context to support the prompt:</p>
<p><a href="/blog/img/octo-easy-mode-21-self-support/mcp-servers.png"><img src="/blog/i/x/octo-easy-mode-21-self-support/mcp-servers.png" alt="MCP Servers" loading="lazy" srcset="/blog/i/600/octo-easy-mode-21-self-support/mcp-servers.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/mcp-servers.webp 1000w, /blog/i/2000/octo-easy-mode-21-self-support/mcp-servers.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>The <a href="https://github.com/OctopusSolutionsEngineering/SelfSupportDemo/issues?q=is%3Aissue%20state%3Aclosed">GitHub repo</a> includes a number of issues that document common problems and their solutions. This effectively serves as a knowledge base for the <code>Self Support</code> process template.</p>
<p>The result of this step is to collect as much information as possible about the deployment, link it to any relevant issues in the knowledge base, and provide guidance on how to resolve the problem.</p>
<p>This process template must be <a href="https://octopus.com/docs/platform-hub/templates/publishing-and-sharing">published and shared to your space</a>. See <a href="/blog/platform-engineering-lunch-2">Practical Platform Engineering in 5 Lunches: 2. Your First Project</a> for more details on publishing and sharing process templates.</p>
<h2 id="adding-the-self-support-process-template-to-the-deployment-process">Adding the self-support process template to the deployment process</h2>
<p>Once the process template is published, you can add it to the deployment process of the <code>Web App</code> project. The process template will add a step to the deployment process that queries a knowledge base for solutions to common problems.</p>
<p>You must define values for the process template parameters:</p>
<ul>
<li><code>SelfSupport.WorkerPool</code>: Set to <code>Hosted Ubuntu</code>.</li>
<li><code>SelfSupport.Claude.ApiKey</code>: Set to your Claude API key.</li>
<li><code>SelfSupport.GitHub.PAT</code>: Set to your GitHub personal access token.</li>
<li><code>SelfSupport.Octopus.ApiKey</code>: Set to your Octopus Agent API key.</li>
<li><code>SelfSupport.RunConditions</code>: Set to <code>#{if Octopus.Deployment.Error}True#{/if}</code>. This sets the value to <code>True</code> if the deployment failed.</li>
</ul>
<p><a href="/blog/img/octo-easy-mode-21-self-support/process-template-parameters.png"><img src="/blog/i/x/octo-easy-mode-21-self-support/process-template-parameters.png" alt="Process template parameters" loading="lazy" srcset="/blog/i/600/octo-easy-mode-21-self-support/process-template-parameters.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/process-template-parameters.webp 1000w, /blog/i/2000/octo-easy-mode-21-self-support/process-template-parameters.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<p>Deploy a new release of the <code>Web App</code> project to the <code>Development</code> environment. The deployment will fail, but this time the self-support step will run and provide guidance on resolving the issue.</p>
<p><a href="/blog/img/octo-easy-mode-21-self-support/report.png"><img src="/blog/i/x/octo-easy-mode-21-self-support/report.png" alt="Report" loading="lazy" srcset="/blog/i/600/octo-easy-mode-21-self-support/report.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/report.webp 1000w, /blog/i/2000/octo-easy-mode-21-self-support/report.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></a></p>
<h2 id="why-this-pattern-supports-platform-engineering-teams">Why this pattern supports Platform Engineering teams</h2>
<p>Defining an AI-based self-support step as a Platform Hub Process Template enables Platform Engineering teams to embed a common, centrally managed solution across all projects. The process template can be updated to improve the prompt or to link to additional MCP servers to embed more context, and is distributed simply by publishing a new version.</p>
<p>The set of GitHub issues is also dynamic. As new solutions are discovered, they can be added to the knowledge base and surfaced to users without requiring any changes to the deployment process.</p>
<p>All of this enables short feedback loops for both Platform Engineering teams and platform users. It reduces the cognitive load by providing a single source of truth for common problems and their solutions, and it reduces the support burden on Platform Engineering teams by enabling users to self-support their deployments. It also improves flow state by allowing users to resolve problems without waiting for support from the Platform Engineering team.</p>
<p>These three dimensions are central to improving <a href="https://queue.acm.org/detail.cfm?id=3595878">Developer Experience (DevEx)</a> and are key to the success of any platform engineering initiative:</p>
<p><img src="/blog/i/x/octo-easy-mode-21-self-support/devex.png" alt="Developer Experience" loading="lazy" srcset="/blog/i/600/octo-easy-mode-21-self-support/devex.webp 600w, /blog/i/1000/octo-easy-mode-21-self-support/devex.webp 810w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="810" height="585"></p>
<h2 id="what-just-happened">What just happened?</h2>
<p>You created a sample project that:</p>
<ul>
<li>Was guaranteed to fail due to a missing prerequisite.</li>
<li>With a runbook that resolved the prerequisite but was not known to the user.</li>
<li>Then added a Platform Hub process template to the deployment process that provided AI-based self-support guidance to the user when the deployment failed.</li>
</ul>
<h2 id="next-steps">Next steps</h2>
<p>To productionize the example, you must <a href="https://octopus.com/docs/platform-hub">configure Platform Hub with your own Git repo</a>. You can then copy the <a href="https://mockgit.octopusdemos.com/browse/usernamegoeshere/platformhubrepo/.octopus/process-templates/self-support.ocl?branch=main">Self Support OCL file</a> to your own Git repository, which will make it available in Platform Hub.</p>
<p>You will also likely want to add additional MCP servers to provide more context to the prompt. Almost every major platform has a supporting MCP server these days.</p>
<p>The prompt used to generate support answers will also need to be tweaked to support any additional MCP servers. You will also likely want to refine the generated responses. Claude provides <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview">prompt engineering documentation</a> to help you build a prompt.</p>]]></content>
    </entry>
</feed>