<?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-08-27</updated>

    <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>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>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>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>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>
<p>:::figure</p>
<p>:img{ src="/blog/img/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" }</p>
<p>:::</p>
<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>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>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>
<p>:::figure</p>
<p>:img{ src="/blog/img/hotfix-myth/recommended-octopus-lifecycles.png" alt="Screenshot of Octopus Deploy interface showing the recommended lifecycles of default and release." loading="lazy" }</p>
<p>:::</p>
<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>
<p>:::figure</p>
<p>:img{ src="/blog/img/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" }</p>
<p>:::</p>
<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>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>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>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>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>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>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>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>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>
<p>:::div{.hint}</p>
<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>]]></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>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>The full pipeline</h2>
<p>Here's how everything connects, from Prometheus alert to Slack notification:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/ai-powered-incident-bot/pipeline.png" alt="Full pipeline: Prometheus alert to Octopus runbook execution" }</p>
<p>:::</p>
<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>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><code class="language-json">{
  "root_cause": "Clear explanation of the problem",
  "confidence": "HIGH|MEDIUM|LOW",
  "remediation_type": "one of the above types",
  "runbook_params": {
    "namespace": "{{.Namespace}}",
    "deployment": "deployment name",
    "key": "env var name — config_update only",
    "value": "new env var value — config_update only",
    "type": "env or secret — config_update only",
    "container": "container name — image_fix only",
    "registry": "registry URL — image_fix only",
    "image": "image name — image_fix only",
    "tag": "image tag — image_fix only",
    "cpu": "recommended CPU request e.g. 500m — resource_increase only",
    "memory": "recommended memory request e.g. 512Mi — resource_increase only",
    "cpu_limit": "recommended CPU limit e.g. 1000m — resource_increase only",
    "memory_limit": "recommended memory limit e.g. 1Gi — resource_increase only",
    "target_revision": "revision number, 0 for previous — deployment_rollback only"
  },
  "manual_steps": ["any manual verification steps needed"],
  "suggested_blast_radius": "single_pod|single_deployment|multiple_deployments|cluster"
}
</code></pre>
<p>The five remediation types the model can choose from and the suggested fix are shown below:</p>
<p>| Type | What it does |
| --- | --- |
| <code>pod_restart</code> | Rolling restart of the affected deployment |
| <code>resource_increase</code> | 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 |
| <code>config_update</code> | Patch a misconfigured environment variable or config map entry |
| <code>image_fix</code> | Roll forward to a corrected image tag |
| <code>deployment_rollback</code> | Roll back to the previous revision with a target-revision override |</p>
<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>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><code class="language-go">func resourceIncreaseTemplate() *RemediationTemplate {
    return &#x26;RemediationTemplate{
        Steps: []RunbookStep{
            {
                Name:     "Update Resource Limits",
                StepType: "kubernetes-script",
                Properties: map[string]string{
                    "scriptBody": "kubectl set resources deployment/$(deployment) -n $(namespace)" +
                        " --requests=cpu=$(cpu),memory=$(memory)" +
                        " --limits=cpu=$(cpu_limit),memory=$(memory_limit)",
                },
            },
            {
                Name:     "Trigger Rollout",
                StepType: "kubernetes-script",
                Properties: map[string]string{
                    "scriptBody": "kubectl rollout restart deployment/$(deployment) -n $(namespace)",
                },
            },
            {
                Name:     "Wait for Rollout",
                StepType: "kubernetes-script",
                Properties: map[string]string{
                    "scriptBody": "kubectl rollout status deployment/$(deployment) -n $(namespace) --timeout=5m",
                },
            },
        },
    }
}
</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><code class="language-yaml">metadata:
  annotations:
    octopus.com/project-id: Projects-42
</code></pre>
<p>If the annotation is absent, the bot asks the operator to pick a project in Slack rather than failing silently.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/ai-powered-incident-bot/slack-alert.png" alt="Slack bot asking the operator to pick a project when annotation is absent" }</p>
<p>:::</p>
<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>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.
:::figure</p>
<p>:img{ src="/blog/img/ai-powered-incident-bot/slack-update-result.png" alt="Slack bot telling the operator about the Runbook execution outcome" }</p>
<p>:::</p>
<p>After execution begins, the bot continues posting status updates in Slack until the runbook succeeds or fails</p>
<h2>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>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>
<p>:::div{.hint}
<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>
<h2>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>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>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>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>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><code class="language-json">require "shellwords"
</code></pre>
<h3>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><code class="language-ruby">AGENT_USER        = "claude"
AGENT_UID         = 1001
AGENT_HOME        = "/home/#{AGENT_USER}"
AGENT_RUNTIME_DIR = "/run/user/#{AGENT_UID}"
</code></pre>
<h3>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><code class="language-ruby">HOST_HOME = File.expand_path("~")
</code></pre>
<h3>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><code class="language-ruby">Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-24.04"
</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><code class="language-ruby">  config.vm.provider "hyperv" do |hv, override|
    override.vm.box = "boxen/ubuntu-24.04"
  end
</code></pre>
<p>:::div{.info}
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>
<h3>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><code class="language-ruby">config.vm.synced_folder ".", "/vagrant", disabled: true
</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><code class="language-ruby">  config.vm.synced_folder File.expand_path("~/Code"), "#{AGENT_HOME}/Code",
    type: "nfs",
    nfs_version: 3,
    nfs_udp: false,
    mount_options: ["actimeo=1", "nolock", "tcp", "rw", "fsc"]
</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><code class="language-ruby">  config.vm.provider "parallels" do |prl, override|
    override.vm.synced_folder File.expand_path("~/Code"), "#{AGENT_HOME}/Code",
    type: nil,
    mount_options: ["share", "rw"]
  end

  config.vm.provider "hyperv" do |hv, override|
    override.vm.synced_folder File.expand_path("~/Code"), "#{AGENT_HOME}/Code",
    type: "smb",
    mount_options: ["rw", "uid=#{AGENT_UID}", "gid=#{AGENT_UID}", "mfsymlinks"]
  end
</code></pre>
<h3>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><code class="language-ruby">config.vm.provider "parallels" do |prl|
    prl.memory = 4096
    prl.cpus   = 6
  end

  config.vm.provider "libvirt" do |lv|
    lv.memory = 4096
    lv.cpus   = 6
  end

  config.vm.provider "virtualbox" do |vb|
    vb.memory = 4096
    vb.cpus   = 6
  end

  config.vm.provider "hyperv" do |hv|
    hv.maxmemory = 4096
    hv.cpus   = 6
  end
</code></pre>
<h3>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><code class="language-ruby">anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY') do
    raise "ANTHROPIC_API_KEY is not set on the host. " \
          "Export it before running vagrant up:\n" \
          "  export ANTHROPIC_API_KEY='your-key-here'"
  end

  config.vm.provision "shell",
    run: "always",
    upload_path: "/home/vagrant/vagrant-shell",
    inline: &#x3C;&#x3C;-SHELL
    set -euo pipefail
    install -o root -g root -m 600 /dev/null /etc/anthropic_api_key.env
    echo "export ANTHROPIC_API_KEY='#{anthropic_api_key}'" > /etc/anthropic_api_key.env
  SHELL
</code></pre>
<p>:::div{.info}
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>
<h3>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><code class="language-ruby">config.vm.provision "file",
    source: "~/.claude.json",
    destination: "/home/vagrant/claude.json.upload"
</code></pre>
<p>We now start building the sandbox environment. This is done in a shell provisioner that runs as root:</p>
<pre><code class="language-ruby">config.vm.provision "shell",
    upload_path: "/home/vagrant/vagrant-shell",
    inline: &#x3C;&#x3C;-SHELL
    set -euo pipefail
</code></pre>
<h3>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><code class="language-bash">    useradd \
      --uid #{AGENT_UID} \
      --home-dir #{AGENT_HOME} \
      --shell /bin/bash \
      -M #{AGENT_USER}
</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><code class="language-bash">    install -d -o #{AGENT_USER} -g #{AGENT_USER} -m 750 #{AGENT_HOME}
</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><code class="language-bash">    cat > /usr/local/sbin/claude-agent &#x3C;&#x3C;'LAUNCHER'
#!/bin/bash
set -euo pipefail

. /etc/anthropic_api_key.env

# --dir is the directory the agent should start in, given relative to the synced
# tree. claude.sh sends the directory it was called from on the host, which is the
# same tree under a different prefix, so the relative path is all that travels.
# Optional: without it the agent starts at the root, which is what a bare
# `sudo /usr/local/sbin/claude-agent` in the guest still does. Anything left on the
# command line afterwards is passed through to claude untouched.
code_root=#{AGENT_HOME}/Code
target=$code_root
rel=

if [ "${1:-}" = --dir ]; then
  if [ "$#" -lt 2 ]; then
    echo "claude-agent: --dir needs a value" >&#x26;2
    exit 2
  fi
  rel=$2
  shift 2
fi

# Validated, but never fatal: a --dir that cannot be honoured should still get you a
# working agent at the root rather than no agent at all. The one thing worth being
# strict about is the shape — --dir names a location inside the synced tree by
# construction, so an absolute path or a .. component is a caller bug, and a caller
# bug that silently starts the agent somewhere outside the tree is worth refusing.
case $rel in
  ""|.)
    ;;
  /*)
    echo "claude-agent: --dir must be relative to $code_root, ignoring '$rel'" >&#x26;2
    ;;
  ..|../*|*/..|*/../*)
    echo "claude-agent: --dir must stay inside $code_root, ignoring '$rel'" >&#x26;2
    ;;
  *)
    if [ -d "$code_root/$rel" ]; then
      target=$code_root/$rel
    else
      echo "claude-agent: $code_root/$rel does not exist, starting in $code_root" >&#x26;2
    fi
    ;;
esac

# The target is handed to the inner shell as a positional argument rather than
# spliced into its script. That script is a single-quoted string, so a path pasted
# into it would be parsed by that shell as code.
exec sudo -u #{AGENT_USER} -H env ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
  bash -lc 'cd "$1" || exit 1; shift; exec claude "$@"' claude "$target" "$@"
LAUNCHER

    chown root:root /usr/local/sbin/claude-agent
    chmod 755 /usr/local/sbin/claude-agent
</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><code class="language-bash">    for skel in /etc/skel/.[!.]*; do
      [ -f "$skel" ] || continue
      install -o #{AGENT_USER} -g #{AGENT_USER} -m 644 \
        "$skel" "#{AGENT_HOME}/$(basename "$skel")"
    done
</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><code class="language-bash">    install -o #{AGENT_USER} -g #{AGENT_USER} -m 600 \
      /home/vagrant/claude.json.upload #{AGENT_HOME}/.claude.json
    rm -f /home/vagrant/claude.json.upload
</code></pre>
<h3>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><code class="language-bash">    mkdir -p /etc/claude-code
    chown root:root /etc/claude-code
    chmod 755 /etc/claude-code
    cat > /etc/claude-code/managed-settings.json &#x3C;&#x3C;'JSON'
{
  "permissions": {
    "deny": [
      "mcp__intellij__execute_terminal_command",
      "mcp__intellij__execute_run_configuration",
      "mcp__intellij__execute_tool",
      "mcp__intellij__build_project",
      "mcp__intellij__run_inspection_kts",
      "mcp__intellij__validate_inspection_kts",
      "mcp__intellij__execute_sql_query",
      "mcp__intellij__xdebug_start_debugger_session",
      "mcp__intellij__xdebug_control_session",
      "mcp__intellij__xdebug_evaluate_expression",
      "mcp__intellij__xdebug_set_variable",
      "mcp__intellij__xdebug_set_breakpoint",
      "mcp__intellij__xdebug_remove_breakpoint",
      "mcp__intellij__xdebug_run_to_line",

      "mcp__intellij__apply_patch",
      "mcp__intellij__create_new_file",
      "mcp__intellij__reformat_file",
      "mcp__intellij__rename_refactoring",

      "mcp__intellij__create_database_connection",
      "mcp__intellij__edit_database_connection",
      "mcp__intellij__test_database_connection",

      "Bash(git add)",
      "Bash(git add:*)",
      "Bash(git commit)",
      "Bash(git commit:*)"
    ]
  },
  "allowManagedPermissionRulesOnly": true,
  "allowManagedHooksOnly": true,
  "disableSideloadFlags": true,
  "env": {
    "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB": "0"
  },
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "excludedCommands": [
      "docker *"
    ],
    "allowManagedReadPathsOnly": true,
    "filesystem": {
      "denyRead": [
        "/etc/*.env",
        "#{AGENT_HOME}/.claude.json"
      ],
      "denyWrite": [
        "#{AGENT_HOME}/.claude.json",
        "#{AGENT_HOME}/.claude/settings*.json",
        "#{AGENT_HOME}/.claude/CLAUDE.md",
        "#{AGENT_HOME}/Code/.claude/settings*.json"
      ]
    },
    "credentials": {
      "files": [
        { "path": "/etc/anthropic_api_key.env", "mode": "deny" },
        { "path": "/etc/github_copilot_token.env", "mode": "deny" },
        { "path": "#{AGENT_HOME}/.claude/settings.json", "mode": "deny" }
      ],
      "envVars": [
        { "name": "ANTHROPIC_API_KEY", "mode": "deny" }
      ]
    }
  }
}
JSON
    chown root:root /etc/claude-code/managed-settings.json
    chmod 444 /etc/claude-code/managed-settings.json
</code></pre>
<p>:::div{.info}
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>
<p>The Claude user settings are defined in <code>/home/claude/.claude/settings.json</code>, effectively disabling all security prompts:</p>
<pre><code class="language-bash">    mkdir -p #{AGENT_HOME}/.claude
    cat > #{AGENT_HOME}/.claude/settings.json &#x3C;&#x3C;'JSON'
{
  "skipDangerousModePermissionPrompt": true,
  "acceptEdits": true,
  "permissions": {
    "defaultMode": "bypassPermissions"
  },
  "sandbox": {
    "autoAllowBashIfSandboxed": true
  }
}
JSON
</code></pre>
<h3>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><code class="language-bash">    cat > #{AGENT_HOME}/.claude/CLAUDE.md &#x3C;&#x3C;'MARKDOWN'
# Filesystem paths in this sandbox

You are running inside a Vagrant guest VM. The user, their IDE, and their
terminal are on the *host* machine. The host directory `#{HOST_HOME}/Code` is
synced to `#{AGENT_HOME}/Code` in this guest — same files, different prefix.

Any path that reaches you from the host side uses the host prefix and is NOT
valid here. This includes:

- the path of the file currently open in the user's IDE
- paths in IDE diagnostics, selections, or attached editor context
- paths the user types or pastes, and paths in output copied from the host

## Translate before every tool call

Rewrite the prefix, keep the rest of the path unchanged:

| Host path | Guest path to use |
| --- | --- |
| `#{HOST_HOME}/Code/&#x3C;rest>` | `#{AGENT_HOME}/Code/&#x3C;rest>` |
| `~/Code/&#x3C;rest>` | `#{AGENT_HOME}/Code/&#x3C;rest>` |
| `#{HOST_HOME}/&#x3C;rest>` (outside `Code`) | not available in this sandbox |

For example, if the IDE reports the open file as
`#{HOST_HOME}/Code/MyProject/src/main.ts`, read and edit
`#{AGENT_HOME}/Code/MyProject/src/main.ts`.

Only `~/Code` is synced. If a host path falls outside it, do not invent a guest
equivalent and do not create the directory to make the path resolve — say the
file is not mounted into the sandbox and ask the user how to proceed.

## Translating back

Use guest paths for every tool call, and when you quote a path in your answer.
The exception is when you are telling the user which file to open on the host
(so their IDE can resolve it) — give the `#{HOST_HOME}/...` form there, and say
which side of the mapping the path belongs to.

The synced folder is mounted read-write, so edits you make under
`#{AGENT_HOME}/Code` appear on the host immediately. These are the user's real
working files, not a throwaway copy — treat them accordingly.

# The account you are running as

You are the `#{AGENT_USER}` user. It is unprivileged on purpose: it has no sudo, no
password, and no membership of the `sudo`, `docker`, `lxd` or `adm` groups. The
`vagrant` account, and its home directory, are not yours to read or write.

So: install nothing system-wide. `apt-get`, `npm install -g` and anything else
needing root will fail, and that is the configuration working, not a problem to
route around. Use a venv, `npm install` into the project, or the rootless Docker
daemon already running for you (`DOCKER_HOST` is set in your environment). If a
task genuinely needs root in this VM, say so and ask the user to run it from the
host with `vagrant ssh`.
MARKDOWN

    chown -R #{AGENT_USER}:#{AGENT_USER} #{AGENT_HOME}/.claude
    chown root:root #{AGENT_HOME}/.claude/settings.json #{AGENT_HOME}/.claude/CLAUDE.md
    chmod 444 #{AGENT_HOME}/.claude/settings.json
    chmod 444 #{AGENT_HOME}/.claude/CLAUDE.md
</code></pre>
<h3>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><code class="language-bash">    touch /home/.mcp.json
    chown root:root /home/.mcp.json
    chmod 444 /home/.mcp.json
</code></pre>
<h3>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><code class="language-bash">    apt-get update -y
    apt-get upgrade -y
    apt-get install -y \
      auditd \
      binfmt-support \
      build-essential \
      curl \
      dbus-user-session \
      fuse-overlayfs \
      git \
      jq \
      python3 \
      python3-pip \
      python3-venv \
      qemu-user-static \
      screen \
      slirp4netns \
      uidmap \
      unzip \
      ufw \
      btop \
      bubblewrap \
      socat
</code></pre>
<h3>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><code class="language-bash">    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
      | dd of=/etc/apt/keyrings/docker.asc
    chmod a+r /etc/apt/keyrings/docker.asc
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release &#x26;&#x26; echo "$VERSION_CODENAME") stable" \
      > /etc/apt/sources.list.d/docker.list
    apt-get update -y
    apt-get install -y docker-ce docker-ce-cli containerd.io docker-ce-rootless-extras
</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><code class="language-bash">    systemctl disable --now docker.service docker.socket containerd.service || true
    systemctl mask docker.service docker.socket
    rm -f /run/docker.sock
</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><code class="language-bash">    grep -q "^#{AGENT_USER}:" /etc/subuid || usermod --add-subuids 165536-231071 #{AGENT_USER}
    grep -q "^#{AGENT_USER}:" /etc/subgid || usermod --add-subgids 165536-231071 #{AGENT_USER}
</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><code class="language-bash">    loginctl enable-linger #{AGENT_USER}
    for _ in $(seq 1 30); do [ -d #{AGENT_RUNTIME_DIR} ] &#x26;&#x26; break; sleep 1; done
    [ -d #{AGENT_RUNTIME_DIR} ] || { echo "XDG_RUNTIME_DIR for #{AGENT_USER} never appeared"; exit 1; }
</code></pre>
<p>Rootless Docker is installed, and the Docker daemon is started as the <code>claude</code> user:</p>
<pre><code class="language-bash">    sudo -u #{AGENT_USER} -H env \
      XDG_RUNTIME_DIR=#{AGENT_RUNTIME_DIR} \
      DBUS_SESSION_BUS_ADDRESS=unix:path=#{AGENT_RUNTIME_DIR}/bus \
      PATH=/usr/bin:/usr/sbin:/bin:/sbin \
      dockerd-rootless-setuptool.sh install
    sudo -u #{AGENT_USER} -H env \
      XDG_RUNTIME_DIR=#{AGENT_RUNTIME_DIR} \
      DBUS_SESSION_BUS_ADDRESS=unix:path=#{AGENT_RUNTIME_DIR}/bus \
      systemctl --user enable --now docker
</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><code class="language-bash">    cat > /etc/profile.d/docker-rootless.sh &#x3C;&#x3C;'PROFILE'
if [ "$(id -u)" = "#{AGENT_UID}" ]; then
  export XDG_RUNTIME_DIR=#{AGENT_RUNTIME_DIR}
  export DOCKER_HOST=unix://#{AGENT_RUNTIME_DIR}/docker.sock
fi
PROFILE
    chown root:root /etc/profile.d/docker-rootless.sh
    chmod 644 /etc/profile.d/docker-rootless.sh
</code></pre>
<h3>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><code class="language-bash">    curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
    apt-get install -y nodejs
</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><code class="language-bash">    npm install -g @anthropic-ai/claude-code
</code></pre>
<h3>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><code class="language-bash">  config.vm.provision "claude-mcp-paths",
    type: "shell",
    run: "always",
    upload_path: "/home/vagrant/vagrant-shell",
    inline: &#x3C;&#x3C;-SHELL
    set -euo pipefail

    command -v jq >/dev/null || { echo "jq is not installed yet; run the main provisioner first"; exit 1; }

    config=#{AGENT_HOME}/.claude.json
    host_prefix=#{Shellwords.escape("#{HOST_HOME}/Code")}
    guest_prefix=#{AGENT_HOME}/Code

    [ -s "$config" ] || { echo "no $config to rewrite"; exit 0; }
    jq -e . "$config" >/dev/null 2>&#x26;1 || { echo "$config is not valid JSON; leaving it alone"; exit 0; }

    tmp=$(mktemp "$config.XXXXXX")
    jq --arg host "$host_prefix" --arg guest "$guest_prefix" '
      def retarget: (. / $host) | join($guest);
      walk(if type == "string" then retarget else . end)
      | if (.projects | type) == "object" then
          .projects = reduce (.projects | to_entries[]) as $e ({};
            .[$e.key | retarget] = ((.[$e.key | retarget] // {}) + $e.value))
        else . end
    ' "$config" > "$tmp"
    chown #{AGENT_USER}:#{AGENT_USER} "$tmp"
    chmod 600 "$tmp"
    mv "$tmp" "$config"

    echo "rewrote MCP host paths: $host_prefix -> $guest_prefix"
  SHELL
</code></pre>
<h3>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><code class="language-bash">  config.vm.provision "claude-trust",
    type: "shell",
    run: "always",
    upload_path: "/home/vagrant/vagrant-shell",
    inline: &#x3C;&#x3C;-SHELL
    set -euo pipefail

    command -v jq >/dev/null || { echo "jq is not installed yet; run the main provisioner first"; exit 1; }

    config=#{AGENT_HOME}/.claude.json
    [ -s "$config" ] || install -o #{AGENT_USER} -g #{AGENT_USER} -m 600 /dev/null "$config"
    jq -e . "$config" >/dev/null 2>&#x26;1 || printf '{}' > "$config"

    tmp=$(mktemp "$config.XXXXXX")
    jq '.projects["#{AGENT_HOME}/Code"] =
          (.projects["#{AGENT_HOME}/Code"] // {}) + {"hasTrustDialogAccepted": true}' \
      "$config" > "$tmp"
    chown #{AGENT_USER}:#{AGENT_USER} "$tmp"
    chmod 600 "$tmp"
    mv "$tmp" "$config"

    echo "trusted workspace: #{AGENT_HOME}/Code"
  SHELL
</code></pre>
<h3>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><code class="language-bash">  config.vm.provision "apparmor-bwrap",
    type: "shell",
    run: "always",
    upload_path: "/home/vagrant/vagrant-shell",
    inline: &#x3C;&#x3C;-SHELL
    set -euo pipefail

    cat > /etc/apparmor.d/bwrap &#x3C;&#x3C;'PROFILE'
# This profile allows everything and only exists to give the
# application a name instead of having the label "unconfined"

abi &#x3C;abi/4.0>,
include &#x3C;tunables/global>

profile bwrap /usr/bin/bwrap flags=(unconfined) {
  userns,

  # Site-specific additions and overrides. See local/README for details.
  include if exists &#x3C;local/bwrap>
}
PROFILE
    chown root:root /etc/apparmor.d/bwrap
    chmod 644 /etc/apparmor.d/bwrap

    apparmor_parser -r -W /etc/apparmor.d/bwrap

    # Fail provisioning loudly if the sandbox still cannot start, rather than
    # leaving the agent with a Bash tool that errors on every command. Probed as the
    # account that will actually run bwrap; this provisioner is ordered after the main
    # one, which is what creates it.
    sudo -u #{AGENT_USER} bwrap --ro-bind / / --unshare-net --dev /dev true
    echo "bwrap sandbox: OK (user namespace + loopback)"
  SHELL
</code></pre>
<h2>Building the sandbox</h2>
<p>Build the sandbox VM with the command:</p>
<pre><code class="language-bash">vagrant up
</code></pre>
<h2>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><code class="language-bash">vagrant ssh -c "sudo /usr/local/sbin/claude-agent --dir MyProject" -- -R 64342:127.0.0.1:64342
</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><code class="language-json">{
  "intellij": {
    "url": "http://127.0.0.1:64342/stream",
    "type": "http"
  }
}
</code></pre>
<p>:::div{.info}
The port is unique on each host, so you will need to replace 64342 with the port used by your IntelliJ MCP server.
:::</p>
<h2>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><code class="language-markdown">Create a script called `gittest.sh`. Populate it with the commands to create a directory called `/tmp/claude-1001/gittest`, run `git init` in the directory, touch a file called `test.txt`, and run `git add`. Then run `gittest.sh`.
</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><code class="language-markdown">Create a Dockerfile that installs git. Mount the directory `/home/claude/Code/MyProject` into the container. Have the container run `touch test.txt` and `git add` in the mounted directory.
</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><code class="language-markdown">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.
</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>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>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>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>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>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>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>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>
<p>:::div{.hint}
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>
<h2>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>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/img/octo-easy-mode-22-policy/policy.png" alt="Self-Support policy" loading="lazy" }</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><code class="language-rego">package self_support_exists

default evaluate := true

# The following are examples of available scoping options:
evaluate if {
    # Scope evaluation by Environment name
    # input.Environment.Name == "&#x3C;environment-name>"
    # Scope evaluation to Space Id
    # input.Space.Id == "Spaces-1"
    # Scope evaluation to multiple Spaces
    # input.Project.Slug in ["&#x3C;project-slug>", "&#x3C;project-slug2>"]
}
</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><code class="language-rego">package self_support_exists

default evaluate := true

# The following are examples of available scoping options:
evaluate if {
    # Scope evaluation by Environment name
    # input.Environment.Name == "&#x3C;environment-name>"
    # Scope evaluation to Space Id
    input.Space.Id == "Spaces-1234"
    # Scope evaluation to multiple Spaces
    # input.Project.Slug in ["&#x3C;project-slug>", "&#x3C;project-slug2>"]
}
</code></pre>
<p>:::div{.warning}
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>
<p>The sample Conditions Rego is this:</p>
<pre><code class="language-rego">package self_support_exists

# Default: Deny all deployments
default result := {"allowed": false}

# Allow: If a specific Process Template is used and not bypassed
result := {"allowed": true} if {
    some step in input.Steps

    # Ensure the step is derived from a Process Template
    step.Source.Type == "Process Template"

    # Target a specific template by its unique slug or ID
    step.Source.SlugOrId == "self-support"

    # Verify this specific step hasn't been added to the skipped list
    not step.Id in input.SkippedSteps

    # Verify the step is enabled
    step.Enabled == true
}
</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/img/octo-easy-mode-22-policy/download-as-json.png" alt="Deployment process JSON" loading="lazy" }</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><code class="language-json">"Actions": [
        {
          "Id": "fc95fd56-778c-42c6-ae35-fe611dfb4619",
          "Name": "Run a Process Template",
          "Slug": "run-a-process-template",
          "ActionType": "Octopus.ProcessTemplate",
          "Notes": null,
          "IsDisabled": false,
          "CanBeUsedForProjectVersioning": false,
          "IsRequired": false,
          "WorkerPoolId": null,
          "Container": {
            "Image": null,
            "FeedId": null,
            "GitUrl": null,
            "Dockerfile": null
          },
</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><code class="language-json">"Properties": {
            "SelfSupport.WorkerPool": "WorkerPools-7706",
            "SelfSupport.Claude.ApiKey": "#{LibraryVariableSet.Claude.ApiKey}",
            "SelfSupport.Octopus.ApiKey": "#{LibraryVariableSet.Octopus.ApiKey}",
            "SelfSupport.GitHub.PAT": "#{LibraryVariableSet.GitHub.PAT}",
            "SelfSupport.RunCondition": "#{if Octopus.Deployment.Error}True#{/if}",
            "Octopus.Action.ProcessTemplate.Reference.Slug": "self-support",
            "Octopus.Action.ProcessTemplate.Reference.VersionMask": "4.X"
          },
</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>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/img/octo-easy-mode-22-policy/publish-policy.png" alt="Publish button" loading="lazy" }</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>
<p>:::div{.hint}
The published policy remains in effect even if the Git repo is reset.
:::</p>
<h2>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/img/octo-easy-mode-22-policy/disable-step.png" alt="Disable step" loading="lazy" }</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/img/octo-easy-mode-22-policy/audit-log-warning.png" alt="Audit log warning" loading="lazy" }</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>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/img/octo-easy-mode-22-policy/block-deployments.png" alt="Block deployments" loading="lazy" }</a></p>
<p>:::div{.warning}
The mock Git repository has likely reset itself at this point, so you will need to reapply any changes to the scope rego.
:::</p>
<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/img/octo-easy-mode-22-policy/deployment-blocked.png" alt="Deployment blocked" loading="lazy" }</a></p>
<h2>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>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>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>
<p>:::div{.hint}
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>
<h2>Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre><code class="language-markdown">* Create a token account called "Mock Token".
* Create a feed called "Docker Hub" pointing to "https://index.docker.io" using anonymous authentication.
* 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".

---

Create a Kubernetes project called "20. Microservice 1", and then:
* Place the project in the "Orchestrator" project group.
* Configure the Kubernetes steps to use client side apply (client side apply is required by the "Mock K8s" target).
* Disable verification checks in the Kubernetes steps (verification checks are not supported by the "Mock K8s" target).
* Enable retries on the Kubernetes step.

---

Create a Kubernetes project called "20. Microservice 2", and then:
* Place the project in the "Orchestrator" project group.
* Configure the Kubernetes steps to use client side apply (client side apply is required by the "Mock K8s" target).
* Disable verification checks in the Kubernetes steps (verification checks are not supported by the "Mock K8s" target).
* Enable retries on the Kubernetes step.

---

Create an Orchestration project called "20. Kubernetes Microservice Orchestration" managing the projects "20. Microservice 1" and "20. Microservice 2".
</code></pre>
<p>:::div{.hint}
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>
<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>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/img/octo-easy-mode-20-microservices/step-start-trigger.png" alt="Deploy a release step start trigger" loading="lazy" }</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>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/img/octo-easy-mode-20-microservices/deploy-a-release-project-versions.png" alt="Deploy a release package selection" loading="lazy" }</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>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/img/octo-easy-mode-21-self-support/platform-engineering-investment.png" alt="Platform Engineering Investment" }</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>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>
<p>:::div{.hint}
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>
<h2>Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre><code class="language-markdown">Create a Kubernetes project called "Web App", and then:
* Add a step called `Check for web-app-secret` 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.
    * 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.
    * The step must run against the "Kubernetes" target tag.
    * Use the namespace "#{Octopus.Environment.Name | ToLower | Replace "[^A-Za-z0-9]" -}#{if Octopus.Deployment.Tenant.Name}#{Octopus.Deployment.Tenant.Name | ToLower | Replace "[^A-Za-z0-9]" -}#{/if}".
    * Uses the "Hosted Ubuntu" worker pool
    * The step runs the "octopusdeploy/worker-tools:6.5.0-ubuntu.22.04" execution container from the "Docker Hub" feed with anonymous authentication.
    * If the secret does not exist, exit with an error.
* The project must require a tenant to run.
* Create 3 tenants named "APAC", "EMEA", and "AMER" that can run the project.
* Create a kubernetes target and scope it to the tenants.

---

Create a runbook called "Create Secret" in the project "Web App" with the description "Creates the secret required by the Web App deployment."
* 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.
* The step runs the "octopusdeploy/worker-tools:6.5.0-ubuntu.22.04" execution container from the "Docker Hub" feed with anonymous authentication.
* The step uses the "Hosted Ubuntu" worker pool.
* Disable server side apply.
* Disable step verification.
* The step runs against the "Kubernetes" target tag.
* The step runs in the namespace "#{Octopus.Environment.Name | ToLower | Replace "[^A-Za-z0-9]" -}#{if Octopus.Deployment.Tenant.Name}#{Octopus.Deployment.Tenant.Name | ToLower | Replace "[^A-Za-z0-9]" -}#{/if}".
* Enable retries on the step.
* The runbook must require a tenant to run.
* The runbook is scoped to the "Development", "Test", and "Production" environments.
</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>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>Configuring Platform Hub</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it:</p>
<pre><code class="language-markdown">Configure the Platform Hub git repo to point to https://mockgit.octopusdemos.com/repo/platformhubrepo using the ".octopus" base path.
</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/img/octo-easy-mode-21-self-support/platfrm-hub-version-control.png" alt="Platform Hub version control" loading="lazy" }</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>
<p>:::div{.warning}
The mock git repo resets after a period of time, so any changes you make will be lost. This is expected behavior.
:::</p>
<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/img/octo-easy-mode-21-self-support/self-support-process-template.png" alt="Process template" loading="lazy" }</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><code class="language-markdown">The deployment #{Octopus.Deployment.Id} in space #{Octopus.Space.Name} for project #{Octopus.Project.Name} has failed.

Your task is to provide guidance on how to troubleshoot and resolve the issue.

Provide a solution to the failure by:

* Getting the Octopus deployment logs and determining which step failed
* Getting the configuration of the deployment steps
* Getting any git diffs from the commits noted in the build information
* If the project is configured with config-as-code, checking the git repo hosting the project configuration for any recent changes
* Checking the GitHub issues at https://github.com/OctopusSolutionsEngineering/SelfSupportDemo for any relevant issues or known bugs that match the failed step

# Notes

* You must only use the MCP servers to gather information.
* You must not run any scripts, execute curl, or run any other CLI tools to gather information.
* You must not list every issue you find in GitHub.
* You must only reference an issue if it is directly related to the current deployment failure.
</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/img/octo-easy-mode-21-self-support/mcp-servers.png" alt="MCP Servers" loading="lazy" }</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>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/img/octo-easy-mode-21-self-support/process-template-parameters.png" alt="Process template parameters" loading="lazy" }</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/img/octo-easy-mode-21-self-support/report.png" alt="Report" loading="lazy" }</a></p>
<h2>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/img/octo-easy-mode-21-self-support/devex.png" alt="Developer Experience" loading="lazy" }</p>
<h2>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>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>
    <entry>
      <title>Act faster with our new Slack integration</title>
      <link href="https://octopus.com/blog/slack-integration" />
      <id>https://octopus.com/blog/slack-integration</id>
      <published>2026-08-13</published>
      <updated>2026-08-13</updated>
      <summary>Octopus now posts to Slack. Send scoped event notifications to the channels your teams already watch, or fire a message from any point in a deployment process.</summary>
      <author>
        <name>Michelle O'Brien, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>If a deployment fails in Octopus and no one is around to see it, does it make a sound?</p>
<p>The gap between an event happening and the right person knowing about it is where deployments quietly stall. We've had consistent customer feedback that Octopus is one of many tools you use, and it's easy to miss when something requires your approval or intervention.</p>
<p>We've built a Slack integration so Octopus can tell your team what needs action, in the tool they already have open. There are two ways to use it:</p>
<ol>
<li>Subscriptions: for event notifications that can be scoped to the teams that care about them</li>
<li>Send a Slack Message step: for Slack messages sent from a specific point in a deployment or runbook process</li>
</ol>
<h2>Subscriptions: our notification engine</h2>
<p><a href="https://octopus.com/docs/administration/managing-infrastructure/subscriptions">Subscriptions</a> are an existing feature that let users subscribe to events in Octopus. In addition to webhooks and email notifications, you can now have these updates sent to a Slack channel.</p>
<p>:img{ src="/blog/img/slack-integration/variable-changes.png" alt="Screenshot showing a subscription that is tracking changes to varaibles in two specfied projects" loading="lazy" }</p>
<p>A couple of suggested Slack channels to help you keep up to date with important events in Octopus without constant monitoring are:</p>
<ul>
<li><code>#deployment-failures</code> that captures all deployment failures in one place</li>
<li><code>#deployments-variables-changed</code> that notifies when a variable has been updated</li>
<li><code>#octopus-api-expiries</code> that gives you warnings prior to API expiry events</li>
<li><code>#octopus-mcp-oauth</code> that informs when MCP authorization tokens are issued</li>
</ul>
<p>:img{ src="/blog/img/slack-integration/variable-changes-notification.png" alt="Screenshot showing a notification in Slack that a variable has changed" loading="lazy" }</p>
<p>If you want your teams to be able to narrow notifications, event filters let you drill down into resources like projects, tags, and environments, so you can update a team channel with events they care about.  Keeping your audience narrow and the notifications relevant prevents a mass muting event that makes the integration ineffective.</p>
<h2>Slack steps made simple</h2>
<p>We have a number of community steps that enable messages to Slack but the set up is more complex, our new Octopus <a href="https://octopus.com/docs/projects/built-in-step-templates/send-slack-message">Send a Slack Message step</a> uses the oAuth integration so all you need to do is choose a channel and a message to send.</p>
<p>:img{ src="/blog/img/slack-integration/slack-step.png" alt="Screenshot showing a process with a slack step" loading="lazy" }</p>
<p>This step is particularly useful when placed immediately before a manual intervention step. The message goes out the moment the deployment reaches that point, so the approver knows to go and unblock it. The message field supports Slack markdown and Octopus variables, so you can include the project, release number, environment, and a link straight to the deployment.</p>
<p>:img{ src="/blog/img/slack-integration/slack-notification.png" alt="Screenshot showing a process with a slack step" loading="lazy" }</p>
<h2>Learn more</h2>
<p><a href="https://octopus.com/docs/administration/managing-infrastructure/slack-integration">Learn more about setting up the Slack integration here</a>.</p>
<h2>What's next for notifications?</h2>
<ul>
<li>We're gauging interest in a Microsoft Teams integration so if you're a Teams team comment below.</li>
<li>Notify and act: we're adding webhooks triggers to runbooks so you can use the webhook event in subscriptions to trigger a runbook. <a href="https://roadmap.octopus.com/c/276-webhook-triggered-runbooks">Follow along here</a>.</li>
</ul>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.8: AI efficiency and effectiveness</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e8" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e8</id>
      <published>2026-08-11</published>
      <updated>2026-08-11</updated>
      <summary>Find out how to measure the outcomes of AI initiatives, why you need to treat your deployment pipelines like a pizza oven, and when adding features makes software less valuable.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In the previous episode, we discussed <a href="https://octopus.com/blog/continuous-delivery-office-hours-e7">modern multi-tenancy</a>, including why application-level multi-tenancy is unnecessary in modern infrastructure. In this episode, we tackle AI efficiency and effectiveness.</p>
<p>There's now broad acceptance that AI doesn't unlock whole-system benefits when it's applied to the wrong places, or when teams lack the foundations to support it.</p>
<p>Read on to find out why AI is creating a new kind of gatekeeper, why there's less pressure to say no to features, and why you need to treat your deployment pipeline like a pizza oven.</p>
<h2>Watch the episode</h2>
<p>You can watch the episode below, or read on to find some of the key discussion points.</p>
<p><a href="https://www.youtube.com/watch?v=ZXbpwcZ78f0">Watch Continuous Delivery Office Hours Ep.8</a></p>
<h2>The infuriating AI gatekeeper</h2>
<p>By now, most of us have been on the receiving end of a terrible interaction with an organization that's put an AI chat tool in front of their support or customer service. We can formalize and explore this problem using the <a href="https://octopus.com/blog/focus-on-end-users-for-ai">beneficiary user and end user</a> types:</p>
<ul>
<li><strong>Beneficiary user</strong>: The person benefiting from making a task easier using AI.</li>
<li><strong>End user</strong>: The person interacting with the AI or its output.</li>
</ul>
<p>Sometimes these are the same person, but in many cases, the beneficiary user enjoys a reduced workload while the end user has to run a chatbot gauntlet, review a 15-page vibe-written strategy document, or deal with a poor automated decision.</p>
<p>The <a href="https://stevefenton.co.uk/blog/2020/12/the-ethics-of-scale/">ethics of scale</a> explains how to use automation responsibly, and the same thinking applies to how we affect other people through our use of AI.</p>
<h2>There's less pressure to say no</h2>
<p>We no longer need to say no to a feature just because we assume AI will make it cheap and fast to build. That removes a pressure many product managers have relied on to keep their roadmap in check, which makes strong product management, backed by a clear product vision, more important than ever. Without it, products become bloated with features, overwhelming users with too many options and losing the simplicity of a curated, opinionated feature set.</p>
<p>Many organizations decline features only because they don't have the capacity to build them all. If these teams lose that natural mechanism for trimming their roadmap, they might add features that don't improve the product for most users. That makes the software less valuable, because it becomes harder to understand, harder to use, and harder to maintain.</p>
<h2>The software delivery pizza oven</h2>
<p>You need <a href="https://octopus.com/blog/developer-productivity-age-ai">certain foundations</a> in place to deliver software in a way that optimizes for feedback. The pizza oven analogy helps illustrate why.</p>
<p>Making a pizza starts with a human process: the dough is hand-stretched, and the toppings are added. Once that's done, the pizza moves onto a conveyor belt that carries it through the oven at a fixed speed, so it's always cooked properly and safe to eat.</p>
<p>Software delivery should work the same way. Humans apply their unique skill and taste to create change, then that change travels through a deployment pipeline that checks it works and is safe to use.</p>
<p>Either way, speed only matters end-to-end. There's no point preparing a pizza if you can't get it through the oven, and no point stacking up cooked pizzas if customers aren't ordering and enjoying them.</p>
<p>Too many teams speed up pizza preparation without a good oven. That's the start of a painful downward spiral toward large-batch, high-risk, high-failure software delivery.</p>
<p>Happy deployments!</p>
<p>:::div{.hint}</p>
<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>
<p>:::</p>]]></content>
    </entry>
    <entry>
      <title>End-to-end Kubernetes deployments with the Octopus Deploy MCP server</title>
      <link href="https://octopus.com/blog/onboarding-with-octopus-deploy-mcp" />
      <id>https://octopus.com/blog/onboarding-with-octopus-deploy-mcp</id>
      <published>2026-08-10</published>
      <updated>2026-08-10</updated>
      <summary>Using the Octopus Deploy MCP server to create a full deployment process for a Kubernetes application</summary>
      <author>
        <name>Kostis Kapelonis, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Last year, we <a href="https://octopus.com/blog/launching-octopus-mcp">launched our MCP server</a> that lets you ask your favorite AI agent about your Octopus Deploy installation. We also showcased some simple scenarios in our <a href="https://www.youtube.com/watch?v=GgpmsKHYR08">quick tutorial video</a>.</p>
<p>In the AI era, things are moving fast. While the MCP server is great at querying an existing Octopus Deploy installation and getting answers that combine multiple tools, several of our customers asked us whether the MCP server can also create new deployments and workflows inside Octopus Deploy.</p>
<p>And the answer is yes. In fact, we accepted the challenge, and in this post you'll learn how you can create a complete end-to-end deployment workflow on an empty Octopus Deploy instance.</p>
<h2>Using the MCP server for Octopus Deploy onboarding</h2>
<p>Our goal is simple. We want to take an empty Octopus Deploy instance (or space) and create a deployment lifecycle for a Kubernetes application using only the MCP server.</p>
<p>We start with an empty space:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/empty.png" alt="Empty Octopus Deploy instance" }</p>
<p>:::</p>
<p>We then create a full workflow from scratch that includes all the main entities of Octopus Deploy (lifecycle, deployment targets, releases, etc.).</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/main-dashboard.png" alt="Octopus Deploy main dashboard" }</p>
<p>:::</p>
<p>The challenge is to use <strong>only</strong> the MCP server. We can still use the Octopus Web Portal to view and verify everything the agent created, but not to edit or update it.</p>
<p>Achieving this goal gives us confidence that the Octopus Deploy MCP server is feature-complete and ready for onboarding scenarios where an AI agent beats working in the UI directly.</p>
<h2>A full deployment workflow from source code to a Kubernetes cluster</h2>
<p>The plan is to create a typical Kubernetes deployment in its simplest form. Source code is stored in GitHub. GitHub Actions compile the code, create a container image, and push it to a registry. The same repository also holds Kubernetes manifests that define how the cluster must handle this container.</p>
<p>Octopus Deploy then takes over and creates a release, which gets deployed to the target cluster:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/deployment-pipeline-diagram.png" alt="From source code in GitHub, through GitHub Actions, to a release deployed by Octopus Deploy to a Kubernetes cluster" }</p>
<p>:::</p>
<p>For simplicity, Octopus Deploy manages the Kubernetes cluster directly, with no <a href="https://octopus.com/blog/argocd-and-octopus">Argo CD integration</a> at all. We'll use the <a href="https://octopus.com/docs/kubernetes/targets/kubernetes-agent">standard Kubernetes agent</a> to push deployments to the cluster.</p>
<h2>Octopus Deploy MCP server installation and access rights</h2>
<p>The first step is to connect the MCP server to your AI agent. We used Claude Code for this guide, but the MCP server should work in a similar way for other AI agents.</p>
<p>To install the MCP server, follow our <a href="https://octopus.com/docs/octopus-ai/mcp/#-installation">installation documentation</a>. At the time of writing, we only offer the <code>stdio</code> transport, so you need to install the MCP server on your workstation or a machine that can reach the Octopus API.</p>
<p>You need to create an API key for agents from the Octopus Web Portal.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/api-key.png" alt="Creating an API key for an agent" }</p>
<p>:::</p>
<p>We strongly advise creating a new agent key for the MCP integration, plus an <a href="https://octopus.com/docs/security/users-and-teams/service-accounts#agent-service-accounts">agent service account</a>. This way you'll be able to audit the agent/MCP actions separately from user-initiated actions in the UI.</p>
<p>The MCP server also supports different permission modes:</p>
<ul>
<li>Read-only means the MCP server only reads information and never changes Octopus Deploy entities (the <code>--read-only</code> flag).</li>
<li>Default mode gives the MCP server read/write access, but not delete.</li>
<li>You must explicitly allow deletions with the <code>--allow-deletes</code> flag.</li>
</ul>
<p>Write and delete actions always show a confirmation prompt, even when allowed. For unattended or scripted access, you can turn the prompt off with the <code>OCTOPUS_SKIP_ELICITATION</code> flag.</p>
<p>For our own scenario, the MCP server needs full write access. You don't normally need <code>--allow-deletes</code> unless you want the agent to clean up resources. If you use the MCP server in a sandbox or test Octopus Deploy instance, feel free to turn it on.</p>
<p>To test the agent connection, simply ask a question about your Octopus Deploy instance:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/simple-question.png" alt="Asking a simple question with the MCP Server" }</p>
<p>:::</p>
<p>This validates that everything works correctly and we can now create our deployment workflow.</p>
<p>You can also use the <a href="https://octopus.com/docs/octopus-rest-api/cli">Octopus CLI</a> alongside the MCP server.</p>
<h2>The deployment lifecycle</h2>
<p>Let's explain how our deployment works. We could do the classic trio of dev/QA/production, but we wanted to show how the MCP server works with different Octopus Deploy entities. So to keep things interesting, we want to model the following promotion flow:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/promotion-flow.png" alt="Example promotion flow" }</p>
<p>:::</p>
<p>All deployments from developers go to QA first. Then we can optionally send them to a load-testing environment to check for performance regressions. This happens for all daily commits on the main branch.</p>
<p>When we want to make a proper release, we create a Git tag. Git tags then follow the expected workflow: they go to staging automatically, and to production after manual approval.</p>
<p>We can describe everything to Octopus Deploy in natural language to create the deployment target and associated lifecycles.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/lifecycles.png" alt="Describing the full deployment lifecycle to the AI agent in natural language" }</p>
<p>:::</p>
<p>After creation, we can use the Octopus Web Portal to verify that what was created matches our expectations:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/two-lifecycles.png" alt="Two lifecycles created" }</p>
<p>:::</p>
<p>We keep iterating like this for all the other Octopus Deploy entities. We ask what we want in natural language, verify the result in the Octopus Web Portal, and ask again for any updates and fixes.</p>
<p>Then we can trigger a deployment by committing a new change in the source code and see Octopus Deploy automatically push to our Kubernetes cluster.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/first-deployment.png" alt="The first Kubernetes deployment" }</p>
<p>:::</p>
<p>Since Octopus Deploy has built-in <a href="https://octopus.com/docs/kubernetes/live-object-status">support for Kubernetes resources</a>, we can also verify that the application is up and running with no errors.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/live-status.png" alt="Kubernetes Live status" }</p>
<p>:::</p>
<p>This concludes the basic deployment scenario. Let's go further by adding configuration settings to our application.</p>
<h2>Different variables per environment</h2>
<p>With the basic deployment in place, we now want to create a more realistic application container that gets different settings per environment. In any non-trivial application, you have configuration settings that differ per environment (e.g. database credentials).</p>
<p>Octopus Deploy already supports <a href="https://octopus.com/docs/projects/variables">variable sets</a> scoped to each deployment target. We can instruct our agent to add variables to each environment in natural language. This is where the power of MCP is evident: the agent adds the variables to Octopus Deploy and makes sure the manifests themselves load them correctly.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/variables.png" alt="Creating variables with MCP" }</p>
<p>:::</p>
<p>For our simple application, the variables are placeholders (there is no real database). But we still want to see each environment get its own values.</p>
<p>Verification is straightforward because with the Kubernetes live object status we can see directly how each environment loads its settings:</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/onboarding-with-octopus-deploy-mcp/configmap.png" alt="Kubernetes configmap from Octopus Deploy variables" }</p>
<p>:::</p>
<p>And that's it!</p>
<p>We now have a complete Kubernetes deployment process, all the way from source code to deployment, created with natural language by our AI agent. We never clicked a create, update, or edit button in the Octopus Web Portal. All the required entities were created via the MCP integration.</p>
<h2>Conclusion</h2>
<p>You've now seen how the Octopus Deploy MCP server can be useful in many kinds of scenarios, not only for asking clarifications about an existing Octopus Deploy instance, but also for creating brand new workflows.</p>
<p>We believe the MCP server opens up Octopus Deploy in ways that weren't possible before, both for automating Octopus itself and for connecting it to the other tools in your workflow. In a future blog post, we'll explore creating situation-specific <a href="https://octopus.com/docs/runbooks">runbooks</a> in a fully automated manner.</p>
<p>The application's source code is available on <a href="https://github.com/kostis-codefresh/hackathon">GitHub</a>. You also need a local Kubernetes cluster as the deployment target if you want to recreate this workflow.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Blue/green deployments</title>
      <link href="https://octopus.com/blog/octo-easy-mode-19-bluegreen" />
      <id>https://octopus.com/blog/octo-easy-mode-19-bluegreen</id>
      <published>2026-08-08</published>
      <updated>2026-08-08</updated>
      <summary>Learn how to create a blue-green deployment project with environment-scoped IIS variables</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Blue/green deployments reduce the risk of production outages by maintaining two identical production environments — one labelled blue and one labelled green — where only one serves live traffic at any time. You deploy a new release to the idle environment, validate it, then switch traffic over. If something goes wrong, you can instantly roll back by returning to the previous active environment.</p>
<p>The <a href="https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_tracking_change_management_immutable_infrastructure.html">AWS Well-Architected Framework - Deploy using immutable infrastructure</a> notes that:</p>
<blockquote>
<p>Deployments are safer because the previous working version has not been changed. You can roll back to it if errors are detected.</p>
</blockquote>
<p>In the <a href="/blog/octo-easy-mode-18-progressive-rollouts">previous post</a>, you created a project that demonstrated a progressive rollout through multiple production environments.</p>
<p>In this post, you will create a sample project that demonstrates blue/green deployments.</p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<p><a href="https://samples.octopus.app/app#/Spaces-1213/projects/blue-green-deployments/deployments?groupBy=Channel">Live Demo</a></p>
<p><a href="https://www.youtube.com/watch?v=AgAIFLG3iq8">Octopus Easy Mode - Blue/Green Deployments</a></p>
<h2>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>
<p>:::div{.hint}
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>
<h2>Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it to create a sample project that deploys an application using the blue/green pattern:</p>
<pre><code class="language-markdown">Create a new blue/green deployment project called "19. Blue-Green deployments".
</code></pre>
<p>The resulting project models a blue/green deployment strategy for a .NET application running on IIS. The same release is deployed to either a <code>Production - Blue</code> or <code>Production - Green</code> environment on alternate deployments, preventing the same environment from receiving a release twice in a row.</p>
<h2>How the blue/green deployment works</h2>
<h3>Lifecycle</h3>
<p>The project creates a custom lifecycle called <code>Blue Green</code> with four phases:</p>
<ul>
<li><code>Development</code></li>
<li><code>Test</code></li>
<li><code>Production Blue</code> (optional) — targets <code>Production - Blue</code></li>
<li><code>Production Green</code> (optional) — targets <code>Production - Green</code></li>
</ul>
<p>Both production phases are marked as optional. This means you can promote a release to either blue or green without being required to deploy to the other. The lifecycle enforces the overall promotion path (you must pass through <code>Development</code> and <code>Test</code> before reaching production), but leaves the choice of active production slot flexible.</p>
<h3>Deployment steps</h3>
<p>The deployment process models the Blue/Green deployment strategy with the <a href="https://library.octopus.com/step-templates/72db001f-ae7f-4a0f-b952-5f80e2fc4cd2/actiontemplate-octopus-check-blue-green-deployment">Octopus - Check Blue Green Deployment</a> community step template.</p>
<p>This step checks the last successful deployment to either production environment and defines an output variable indicating if the same production environment has been deployed to twice in a row.</p>
<p>The output variable is then consumed by a subsequent step. In this example, the second step is a manual intervention step that warns the user if the same production environment has been deployed twice in a row and requires manual approval to continue.</p>
<p>Towards the end of the deployment process, a step <a href="https://octopus.com/integrations/octopus/block-release-progression">blocks the release from being promoted</a> to the other production environment after a successful deployment, preventing the same release from being deployed to both stacks.</p>
<h2>Edge cases</h2>
<p>The Blue/Green deployment pattern is a simple concept with a thousand edge cases. It is trivial to imagine scenarios in which the pattern of alternating deployments between the Blue and Green environments must be broken by two sequential deployments to the same production environment.</p>
<p>For this reason, the <code>Octopus - Check Blue Green Deployment</code> step does not block deployments to the same production environment twice in a row. It only produces an output variable that can be consumed by later steps to enforce the desired behavior.</p>
<h2>Comparing tenants and environments</h2>
<p>This example used environments to represent the blue and green production stacks. It is also possible to use tenants to represent the blue and green stacks. However, there are benefits to using environments:</p>
<ul>
<li>Environments are easier to visualize in the Octopus UI</li>
<li>The ability to block release progression after a successful deployment is only available for environments, not tenants</li>
</ul>
<p>For these reasons, environments are the recommended approach for modeling blue/green deployments in Octopus.</p>
<h2>What just happened?</h2>
<p>You created a sample project with:</p>
<ul>
<li>Two production environments — <code>Production - Blue</code> and <code>Production - Green</code> — each representing an independent production stack</li>
<li>A custom <a href="https://octopus.com/docs/releases/lifecycles">lifecycle</a> called <code>Blue Green</code> that promotes releases through <code>Development</code>, <code>Test</code>, and optionally to either production slot</li>
<li>A validation step that detects and warns when a release is about to be deployed to the same production environment twice in a row, helping to enforce alternating blue/green deployments</li>
<li>A manual approval gate for all production environments, with a contextual warning if the sequential deployment check fires</li>
<li>A step that blocks release progression after a successful production deployment, preventing the same release from being deployed to both slots</li>
</ul>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Progressive Rollout</title>
      <link href="https://octopus.com/blog/octo-easy-mode-18-progressive-rollouts" />
      <id>https://octopus.com/blog/octo-easy-mode-18-progressive-rollouts</id>
      <published>2026-08-07</published>
      <updated>2026-08-07</updated>
      <summary>Learn how to create a progressive deployment project</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Progressive rollouts allow DevOps teams to deploy a release to a small subset of production users before rolling it out to the entire user base. This approach reduces risk by allowing teams to validate the release in production and catch any issues before they affect all users. Typically, the rollout automatically promotes a new version of an application to increasingly larger percentages of production users, like 10%, 50%, and finally 100%. If there is an error, the rollout is halted.</p>
<p>The <a href="https://docs.aws.amazon.com/wellarchitected/latest/devops-guidance/dl.ads.3-use-staggered-deployment-and-release-strategies.html">AWS Well-Architected framework recommends staggered deployments</a>, noting that:</p>
<blockquote>
<p>These techniques contribute to safer and more reliable software deployment and release processes.</p>
</blockquote>
<p>In the <a href="/blog/octo-easy-mode-17-claude">previous post</a>, you created a project that used a Claude agent step to categorize commits.</p>
<p>In this post, you will create a sample project that demonstrates a progressive rollout through multiple production environments.</p>
<p><a href="https://www.youtube.com/watch?v=dQXp_i6YyiY">Octopus Easy Mode - Progressive Rollout</a></p>
<p><a href="https://samples.octopus.app/app#/Spaces-1213/projects/progressive-rollout">Live Demo</a></p>
<p><a href="/blog/easymode">Return to the series index.</a></p>
<h2>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>
<p>:::div{.hint}
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>
<h2>Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it to create a sample project with a progressive rollout:</p>
<pre><code class="language-markdown">Create a new progressive deployment project called "18. Progressive rollout".
</code></pre>
<p>The resulting project models a gradual production rollout by promoting the same release through progressively larger slices of production.</p>
<p>The AI Assistant creates a lifecycle with the environments <code>Prod 10</code>, <code>Prod 50</code>, and <code>Prod 100</code>. The lifecycle captures the different stages of the rollout as a percentage of production traffic, enforces the deployment order, and has the project deploy a release to each environment in turn.</p>
<h2>How the progressive rollout works</h2>
<p>The project creates a custom lifecycle called <code>Progressive</code> with four phases:</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>Each lifecycle phase targets a single environment, and the project uses a runbook to explicitly trigger the next deployment after the current one succeeds.</p>
<p>The deployment process starts with a <code>Deploy App</code> step that simulates deploying an application by printing <code>Deploying app</code> to the task log. It is followed by a <code>Simulate Failure</code> step that acts as a validation gate. This step checks the prompted variable <code>Project.SimulateFail</code>, and if it is set to <code>True</code>, the deployment exits with an error and the rollout stops.</p>
<p>If the validation step succeeds, the process runs a community step template called <code>Run Octopus Deploy Runbook</code>. This step starts a runbook named <code>Deploy Release</code> to promote the current release to the next environment. This works around a limitation where Octopus prevents a deployment to the next environment until the current one is complete, so you cannot trigger a deployment to <code>Prod 50</code> while the <code>Prod 10</code> deployment is still running. By having a runbook trigger the deployment after a short delay, we can be sure the current deployment has completed before the next one starts.</p>
<p>The <code>Run Octopus Deploy Runbook</code> step is configured to run in the <code>Prod 10</code> and <code>Prod 50</code> environments. It dynamically chooses the next environment with the following logic:</p>
<ul>
<li>When the current environment is <code>Prod 10</code>, it triggers a deployment to <code>Prod 50</code></li>
<li>When the current environment is <code>Prod 50</code>, it triggers a deployment to <code>Prod 100</code></li>
</ul>
<p>The step also passes the current release ID into the runbook as the prompted variable <code>Project.Release.Id</code>, ensuring the same release is promoted through each stage of the rollout.</p>
<p>The runbook itself contains a single <code>Sleep</code> step that waits for 60 seconds before using the Octopus API to create the next deployment. This pause allows the current deployment to complete before the next rollout stage begins.</p>
<p>In practice, the rollout looks like this:</p>
<ul>
<li>You create a release and deploy it to <code>Development</code></li>
<li>You promote the release to <code>Prod 10</code></li>
<li>The <code>Run Octopus Deploy Runbook</code> step automatically starts the <code>Deploy Release</code> runbook</li>
<li>The runbook waits 60 seconds and then creates a deployment of the same release to <code>Prod 50</code></li>
<li>When the <code>Prod 50</code> deployment succeeds, the same pattern is used to create the final deployment to <code>Prod 100</code></li>
<li>If there are any failures, the rollout stops</li>
</ul>
<h2>Customizing the rollout</h2>
<p>The <code>Deploy Release</code> runbook initiates a deployment to the next production environment after a short delay. This may be customized to instead <a href="https://octopus.com/docs/projects/project-triggers/scheduled-deployment-trigger">schedule a deployment at a specific time</a>, which allows the rollout to be paused for a longer period of time before continuing. You could, for example, only roll out to 100% of production traffic during off-peak hours, or after the release has been validated in <code>Prod 50</code> for a full day.</p>
<p>You may also consider <a href="https://octopus.com/docs/releases/prevent-release-progression">preventing release progression</a> if a deployment fails. This ensures that a failed release cannot be promoted to the next environment until the issue is resolved. A blocked release will also prevent any scheduled deployments from taking place.</p>
<h2>Comparing tenants and environments</h2>
<p>This example used environments to represent progressive rollouts. It is also possible to use tenants to represent progressive rollouts. However, there are benefits to using environments:</p>
<ul>
<li>Environments are easier to visualize in the Octopus UI</li>
<li>Lifecycles enforce the progression of releases through environments, which in turn progressively advance the rollout</li>
<li>The ability to block release progression after a successful deployment is only available for environments, not tenants</li>
</ul>
<p>For these reasons, environments are the recommended approach for modeling progressive rollouts in Octopus.</p>
<h2>What just happened?</h2>
<p>You created a sample project with:</p>
<ul>
<li>A custom <a href="https://octopus.com/docs/releases/lifecycles">lifecycle</a> called <code>Progressive</code> that promotes releases through <code>Development</code>, <code>Prod 10</code>, <code>Prod 50</code>, and <code>Prod 100</code></li>
<li>A scripted deployment process that simulates an application deployment and then validates the result before continuing</li>
<li>A prompted variable that can intentionally fail the validation step to stop the rollout</li>
<li>A community step template that runs a <code>Deploy Release</code> runbook to promote the same release to the next production environment</li>
<li>A runbook with a delayed API call that chains the rollout from <code>Prod 10</code> to <code>Prod 50</code>, and then from <code>Prod 50</code> to <code>Prod 100</code></li>
</ul>
<h2>What's next?</h2>
<p>The <a href="/blog/octo-easy-mode-19-bluegreen">next step</a> is an example of blue/green deployments.</p>]]></content>
    </entry>
    <entry>
      <title>How to Stream Octopus Audit Logs to Grafana Cloud With OpenTelemetry</title>
      <link href="https://octopus.com/blog/stream-audit-logs-to-grafana-cloud" />
      <id>https://octopus.com/blog/stream-audit-logs-to-grafana-cloud</id>
      <published>2026-08-06</published>
      <updated>2026-08-06</updated>
      <summary>Stream Octopus audit logs to Grafana Cloud over OpenTelemetry, query them with LogQL, and get a Slack alert the moment something gets deleted.</summary>
      <author>
        <name>Jubril Oyetunji, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Your continuous deployment platform already knows exactly who changed what, and when. If someone edited a variable or deleted an environment, Octopus records all of it.</p>
<p>For most teams, centralizing audit logs in the same SIEM tool they already watch is more than an added advantage, it is a compliance requirement, and "audit log to SIEM" stays a busy search term precisely because so many tools make it harder than it should be.</p>
<p>In this guide you will stream Octopus audit events into Grafana Cloud over OpenTelemetry, query them with LogQL, and fire a Slack alert the moment something is deleted.</p>
<p>Because the transport is OpenTelemetry, you can take what you learn here and stream the same events to any OpenTelemetry destination, whether that is Datadog, Honeycomb, Elastic, or a self-hosted collector."</p>
<h2><strong>Where Octopus audit data lives</strong></h2>
<p>Octopus gives you three ways to reach audit data, and it helps to know which one you are reaching for.</p>
<ul>
<li><strong>The Audit tab</strong> (Configuration, then Audit) is the in-product view. It is great for browsing individual events, filtering by user or date, and expanding a single change to see exactly what was modified.</li>
<li><strong>The Audit Stream</strong> pushes those same events out to an external system as they happen. This is the path this article covers. It supports OpenTelemetry (OTLP) alongside direct integrations for Splunk and Sumo Logic.</li>
<li><strong>Compliance Reports</strong>, also in Platform Hub, are the purpose-built path for Governance, Risk, and Compliance (GRC) reporting. If your primary need is producing GRC evidence rather than raw log search, that is where to look. It complements the streaming setup here rather than replacing it.</li>
</ul>
<p>In other words, the Audit tab and Audit Stream give you the raw events, and Compliance Reports streamline the reporting on top.</p>
<h2><strong>Architectural overview</strong></h2>
<p>Before we hop into a demo, It is worth getting a grasp on how this will all work.</p>
<p>Fundamentally, Octopus emits audit events over OTLP. A collector receives them and forwards them to Grafana Cloud, where they land in Loki and become queryable in Explore. From there, an alert rule watches for events and notifies Slack.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/architecture-diagram.png" alt="A diagram showing Octopus using OLTP to ngrok, which fronts Grafana Alloy and Grafana Cloud." loading="lazy" }</p>
<p>::figcaption[Octopus streams over OTLP, Grafana Alloy forwards to Grafana Cloud, and Loki becomes the searchable, alertable home for your audit trail.]</p>
<p>:::</p>
<h2><strong>Prerequisites</strong></h2>
<ul>
<li>An Octopus instance with the Audit Stream available.</li>
<li>A free <a href="https://grafana.com/auth/sign-up/create-user">Grafana Cloud</a> account.</li>
<li><a href="https://grafana.com/docs/alloy/latest/set-up/install/">Grafana Alloy</a>, the OpenTelemetry Collector distribution Grafana recommends, running as the receiver and forwarder.</li>
</ul>
<p>You might ask, "<em>why put a collector in the middle instead of pointing Octopus straight at Grafana Cloud?</em>" Simple. For reliability and control.</p>
<p>The collector batches records, holds the Grafana Cloud credentials in one place, and gives you a single spot to add processing or route to a second backend later. It also keeps Octopus configuration simple: Octopus only ever talks to the collector.</p>
<h2><strong>Step 1: Set up the Grafana Cloud destination</strong></h2>
<p>A free tier Grafana Cloud account includes 50 GB of log ingestion per month with 14 days of retention, which is plenty for an audit trail demo.</p>
<p>Once your stack exists, open the connection details for the OTLP endpoint. In the Grafana Cloud portal, go to your stack and find the OpenTelemetry tile, or open the stack's OTLP connection page directly.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/grafana-otlp-connection-details.png" alt="Grafana OLTP connection details page" loading="lazy" }</p>
<p>::figcaption[The OTLP connection page gives you the gateway endpoint and your instance ID. Generate an API token here to get the third value.]</p>
<p>:::</p>
<p>You are after the following three values:</p>
<ul>
<li><strong>OTLP Endpoint</strong>, something like <code>https://otlp-gateway-prod-us-west-0.grafana.net/otlp</code>. The region in that hostname will match your stack.</li>
<li><strong>Instance ID</strong>, a numeric value such as <code>1714216</code>. This is the username half of the credentials.</li>
<li><strong>API Token</strong>. Click <strong>Generate now</strong>, give the token a name like <code>octopus-audit-stream</code>, keep the default scopes (which include <code>logs:write</code>), and create it. Copy the token, it is shown only once. This is the password half.</li>
</ul>
<p>Logs sent over OTLP land in Loki. Grafana Cloud promotes the OpenTelemetry <code>service.name</code> resource attribute to the Loki stream label <code>service_name</code>, which is how you will find the data later. Octopus sets this to <code>Octopus Deploy</code>, so your query will start with <code>{service_name="Octopus Deploy"}</code>.</p>
<h2><strong>Step 2: Configure the OpenTelemetry collector (Grafana Alloy)</strong></h2>
<p>In Grafana Alloy, create the configuration file. It does three jobs: receive OTLP from Octopus, batch the records, and export them to Grafana Cloud with basic authentication.</p>
<pre><code class="language-bash">cat > config.alloy &#x3C;&#x3C;'EOF'
// 1. Receive OTLP over HTTP (Octopus sends http/protobuf) and gRPC.
otelcol.receiver.otlp "octopus" {
  http {
    endpoint = "127.0.0.1:4318"
  }

  grpc {
    endpoint = "127.0.0.1:4317"
  }

  output {
    logs = [otelcol.processor.batch.default.input]
  }
}

// 2. Batch records before export to reduce request volume.
otelcol.processor.batch "default" {
  output {
    logs = [otelcol.exporter.otlphttp.grafana_cloud.input]
  }
}

// 3. Authenticate to Grafana Cloud with the stack instance ID and API token.
otelcol.auth.basic "grafana_cloud" {
  username = "YOUR_INSTANCE_ID"
  password = sys.env("GRAFANA_CLOUD_API_TOKEN")
}

// 4. Export to the Grafana Cloud OTLP gateway. Logs land in Loki.
otelcol.exporter.otlphttp "grafana_cloud" {
  client {
    endpoint = "https://otlp-gateway-prod-us-west-0.grafana.net/otlp"
    auth     = otelcol.auth.basic.grafana_cloud.handler
  }
}
EOF
</code></pre>
<p>Walking through the two values that matter most:</p>
<ul>
<li>The exporter <code>endpoint</code> is the OTLP gateway from your connection details</li>
<li>The <code>otelcol.auth.basic</code> block is the authentication. Grafana Cloud expects HTTP Basic auth where the username is your instance ID and the password is the API token. Replace <code>YOUR_INSTANCE_ID</code> with the numeric instance ID, and keep the token out of the file by reading it from an environment variable.</li>
</ul>
<p>Start Alloy with the token in the environment:</p>
<pre><code class="language-bash">export GRAFANA_CLOUD_API_TOKEN='glc_your_token_here'
alloy run config.alloy --storage.path=./alloy-data
</code></pre>
<p>Alloy logs that it is listening. You should see the OTLP servers come up:</p>
<pre><code class="language-bash">level=info msg="Starting GRPC server" component_id=otelcol.receiver.otlp.octopus endpoint=127.0.0.1:4317
level=info msg="Starting HTTP server" component_id=otelcol.receiver.otlp.octopus endpoint=127.0.0.1:4318
</code></pre>
<p>Confirm the receiver accepts data before wiring up Octopus:</p>
<pre><code class="language-bash">curl -sw "\nHTTP %{http_code}\n" -X POST http://127.0.0.1:4318/v1/logs \
  -H "Content-Type: application/json" \
  -d '{"resourceLogs":[{"scopeLogs":[{"logRecords":[{"body":{"stringValue":"hello"}}]}]}]}'
</code></pre>
<p>A <code>HTTP 200</code> means Alloy took the record. If it also reached Grafana Cloud, you will see it in Explore in a moment.</p>
<h3><strong>Expose the collector so Octopus can reach it</strong></h3>
<p>Octopus Cloud needs a public URL to send to. For a local test, ngrok is the quickest way to expose the Alloy HTTP receiver, the same technique the <a href="https://octopus.com/blog/elastic-otel">Octopus and Elastic walkthrough</a> uses:</p>
<pre><code class="language-bash">ngrok http 4318
</code></pre>
<p>ngrok prints a public HTTPS URL such as <code>https://4202-12-17-71-220.ngrok-free.app</code>. That, with <code>/v1/logs</code> appended, is what Octopus will target. In a permanent deployment you would run Alloy on a host Octopus can reach directly and skip the tunnel.</p>
<h2><strong>Step 3: Point Octopus at the collector</strong></h2>
<p>In Octopus, go to <strong>Configuration</strong> > <strong>Audit</strong>, and click <strong>Stream Audit Log</strong>. Choose <strong>OpenTelemetry</strong> as the provider.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/octopus-audit-stream-providers.png" alt="Octopus audit stream providers" loading="lazy" }</p>
<p>::figcaption[Octopus supports OpenTelemetry, Splunk, and Sumo Logic out of the box. OpenTelemetry is the one that keeps you vendor-neutral.]</p>
<p>:::</p>
<p>Fill in the OpenTelemetry fields:</p>
<ul>
<li><strong>OpenTelemetry Endpoint URL</strong>: Your collector's log endpoint, which is the ngrok URL with <code>/v1/logs</code> on the end, for example <code>https://4202-12-17-71-220.ngrok-free.app/v1/logs</code>.</li>
<li><strong>OTLP Protocol</strong>: <code>HTTP/protobuf</code>.</li>
<li><strong>Secret</strong>: Leave this empty. Authentication to Grafana Cloud is handled by the collector, so Octopus does not need to send any token. This is exactly why the collector sits in the middle.</li>
</ul>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/octopus-audit-stream-otel-config.png" alt="Octopus audit stream OTel config" loading="lazy" }</p>
<p>::figcaption[The endpoint points at the collector, the protocol is HTTP/protobuf, and no secret is needed because the collector authenticates to Grafana Cloud.]</p>
<p>:::</p>
<p>Click <strong>Save</strong>.</p>
<p>The <strong>Stream Audit Log</strong> button now shows a green check, which means the stream is active and new events will flow. Historical events are not backfilled, only events from this point forward are streamed.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/octopus-audit-stream-active.png" alt="Octopus audit stream active" loading="lazy" }</p>
<p>::figcaption[A green check confirms the stream is live.]</p>
<p>:::</p>
<h2><strong>Step 4: Generate events and verify in Grafana Cloud</strong></h2>
<p>Trigger a few audited actions so there is something to see. Anything that creates, modifies, or deletes a resource works. For this walkthrough, create an environment, edit it, then delete it. Each of those is a separate audit event.</p>
<p>Now open <strong>Explore</strong> in Grafana Cloud, select your logs data source, and run:</p>
<pre><code class="language-bash">{service_name="Octopus Deploy"}
</code></pre>
<p>The events arrive within seconds.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/grafana-explore-audit-events.png" alt="Explore audit events in Grafana" loading="lazy" }</p>
<p>::figcaption[Octopus audit events in Grafana Cloud. The expanded record shows a deleted environment, the Category label, and the exact field-level differences.]</p>
<p>:::</p>
<p>Expand a record and you will see the labels Octopus attaches to every event. These are what make the data useful:</p>
<ul>
<li><code>service_name</code> = <code>Octopus Deploy</code></li>
<li><code>event_name</code> = <code>octopus.audit</code></li>
<li><code>Category</code> = <code>Created</code>, <code>Modified</code>, or <code>Deleted</code></li>
<li><code>Username</code> = the account that made the change</li>
<li><code>IpAddress</code> = where the request came from</li>
<li><code>SpaceId</code> and the resource ID, such as <code>EnvironmentId</code></li>
<li><code>severity_text</code> = <code>Information</code></li>
</ul>
<p>Those labels let you slice the audit trail without parsing message text. If you want every change a specific person made? Filter on <code>Username</code>. And if you want only deletions? Filter on <code>Category</code>:</p>
<pre><code class="language-bash">{service_name="Octopus Deploy"} | Category=`Deleted`
</code></pre>
<p>That single query is the foundation of the alert you are about to build.</p>
<h2><strong>Step 5: Sending Slack alerts</strong></h2>
<p>A searchable audit trail is useful but an audit trail that pages you when something sensitive happens is better. Grafana Alerting has a native Slack integration, so no extra service is needed.</p>
<p>First, create the Slack contact point. In Slack, add an <a href="https://api.slack.com/messaging/webhooks">Incoming Webhook</a> for the channel you want, a channel like <code>#grc</code> is a natural home for this.</p>
<p>In Grafana, go to <strong>Alerts &#x26; IRM</strong> > <strong>Alerting</strong> > <strong>Contact points</strong> then add a contact point, choose <strong>Slack</strong>, and paste the webhook URL. Keep that URL secret, anyone who has it can post to your channel.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/grafana-slack-contact-point.png" alt="Grafana Slack contact point" loading="lazy" }</p>
<p>::figcaption[Creating the Slack contact point. With a webhook URL, you can leave the recipient and token fields empty.]</p>
<p>:::</p>
<p>Next, create the alert rule. Point it at your logs data source and use a query that counts delete events over a short window. Group the count by the labels you want in the alert, so they survive the aggregation and can be used in the message:</p>
<pre><code class="language-text">sum by (Category, Username, IpAddress, SpaceId, EnvironmentId) (
  count_over_time({service_name="Octopus Deploy"} | Category=`Deleted` [5m])
)
</code></pre>
<p>Add a threshold condition of <code>is above 0</code>, so the rule fires whenever a deletion shows up. Give the rule a label like <code>team = grc</code> and route that label to your Slack contact point in the notification policy.</p>
<p>Set the rule's summary annotation to reference those labels, so the Slack message names the event rather than showing <code>[no value]</code>. Grafana exposes the query's labels as <code>$labels</code>:</p>
<pre><code class="language-text">Octopus audit alert: {{ $labels.Category }} event by {{ $labels.Username }} from {{ $labels.IpAddress }}
</code></pre>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/grafana-alert-rule-detail.png" alt="Grafana alert rule detail" loading="lazy" }</p>
<p>::figcaption[The alert rule counts Octopus delete events and fires when the count goes above zero.]</p>
<p>:::</p>
<p>Delete an environment to test it. Within an evaluation cycle the rule moves to Firing, and the instance shows its destination is the Slack contact point.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/grafana-alert-instances.png" alt="Grafana alert instances" loading="lazy" }</p>
<p>::figcaption[One firing instance, labelled team=grc, routed to the Slack contact point.]</p>
<p>:::</p>
<p>A message lands in your channel naming the event, the user, and the source IP.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/stream-audit-logs-to-grafana-cloud/slack-grc-alert.png" alt="Slack GRC alert" loading="lazy" }</p>
<p>:::</p>
<h2><strong>Use any backend</strong></h2>
<p>The reason to do this over OpenTelemetry rather than a proprietary integration is portability. Nothing in the Octopus configuration is Grafana-specific. Octopus speaks OTLP to a collector, full stop.</p>
<p>To send the same audit stream to Datadog, Honeycomb, Elastic, or a self-hosted backend, you change the collector's exporter and leave everything else alone. The receiver stays the same, the Octopus Audit Stream config stays the same, additionally you can fan out to two destinations at once by listing two exporters.</p>
<p>That is the whole point of standardizing on OpenTelemetry.</p>
<h2><strong>Your audit trail, wherever you watch it</strong></h2>
<p>Octopus has been recording who did what since long before you turned this on. The work here was not generating the data, but moving it to where your team looks. Because the transport is OpenTelemetry, that move is a single endpoint away from any SIEM tool you run.</p>
<p>If you want to try it, spin up a <a href="https://grafana.com/auth/sign-up/create-user">free Grafana Cloud account</a> and read the <a href="https://octopus.com/docs/security/users-and-teams/auditing/audit-stream">Audit Stream documentation</a> for the exact fields.</p>
<p>If your focus is governance and compliance reporting, look at <a href="https://octopus.com/docs/platform-hub/compliance-reports">Compliance Reports</a>.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>MCP Easy Mode</title>
      <link href="https://octopus.com/blog/mcp-easy-mode" />
      <id>https://octopus.com/blog/mcp-easy-mode</id>
      <published>2026-08-05</published>
      <updated>2026-08-05</updated>
      <summary>Learn how to run your runbooks as MCP tools</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>What you'll learn in this post:</p>
<ul>
<li>The challenges of maintaining collections of MCP servers.</li>
<li>Why simple tools can lead to higher token usage.</li>
<li>Introducing the <a href="https://github.com/OctopusSolutionsEngineering/OctopusEasyModeMCP">Octopus Easy Mode MCP</a> server.</li>
<li>Demonstrating how the Octopus Easy Mode MCP server can be used to expose runbooks as MCP tools.</li>
</ul>
<p>All examples in this post are copy and paste prompts, so you will have a working MCP server executing runbooks in 30 minutes.</p>
<p>:img{ src="/blog/img/mcp-easy-mode/diagram.png" alt="Architecture diagram" }</p>
<h2>Introduction</h2>
<p><a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro">Model Context Protocol</a> (MCP) servers are the new AI layer specifically designed to support agentic workflows. The protocol has gained an enormous amount of support, and it is reasonable to expect that major software vendors will provide an MCP server. Exposing a combination of MCP servers to your AI harness allows you to complete complex tasks while leaving decisions about which service to call to the AI. Agentic workflows are very much founded on the idea that LLMs can ground themselves with trusted, external sources of truth, use tools to interact with the world, and consume feedback to make decisions about how to complete a task.</p>
<h2>Challenges with MCP servers</h2>
<p>However, maintaining collections of MCP servers is a non-trivial challenge. End users have to maintain a list of servers, which is challenging to standardize across an organization, and with the added complexity of embedding credentials in the <code>mcp.json</code> file. Like any desktop software, local MCP servers also need to be kept up to date, while remote MCP servers must be hosted and maintained by a dedicated team.</p>
<p>Exposing a collection of general-purpose tools to an LLM also increases token use. LLMs are quite capable these days of reasoning about how to complete a task, but this comes at the cost of lengthy chain-of-thought reasoning as the LLM works out how to combine otherwise disparate tools.</p>
<p>And there will always be gaps in MCP server coverage. A server may not be available, network security rules may prevent access, or the task to be automated may be too complex and bespoke for an LLM to reliably complete.</p>
<h2>Octopus Easy Mode MCP server</h2>
<p>The <a href="https://github.com/OctopusSolutionsEngineering/OctopusEasyModeMCP">Octopus Easy Mode MCP server</a> is a community project that takes a different approach to traditional MCP servers by exposing runbooks as <a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools">MCP tools</a>. This allows any process that can be automated with a runbook to be executed by an LLM, which has a number of benefits:</p>
<ul>
<li>One MCP server can execute any process that can be automated with a runbook.</li>
<li>Octopus orchestrates complex processes as a series of deterministic steps.</li>
<li>Token count is reduced as the LLM can delegate the execution of complex processes to the Octopus server, rather than having to generate all the steps itself.</li>
<li>Octopus provides security and auditing features, making it possible to restrict and trace the LLMs actions.</li>
<li>There is no longer any specialized knowledge required to create an MCP server – anyone who can create a runbook can expose it as an MCP tool.</li>
</ul>
<p>In this post, you'll learn how to use the Octopus Easy Mode MCP server to expose a runbook as an MCP tool and then use that tool to automate a process.</p>
<h2>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>
<p>:::div{.hint}
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>
<h2>Creating the runbook</h2>
<p>The first step is to create a runbook that will be exposed as an MCP tool. In this example, you'll create a runbook that will print the current date and time.</p>
<p>Run the following prompt in the Octopus AI Assistant:</p>
<pre><code class="language-markdown">Create a project called "Easy Mode MCP" and then:
* Add a runbook description
* Add a runbook called "Get Current Time".
* The runbook must have a single script step that echoes the current time with the PowerShell command `Get-Date`. 
* Create a single environment called "MCP". Do not create any other environments.
* Configure the runbook to only run in the MCP environment.
</code></pre>
<p>:img{ src="/blog/img/mcp-easy-mode/ai-assistant-icon.png" alt="AI Assistant Icon" loading="lazy" }</p>
<p>:img{ src="/blog/img/mcp-easy-mode/ai-assistant.png" alt="AI Assistant Interface" loading="lazy" }</p>
<p>This prompt creates a project to host the runbooks used by the Easy Mode MCP server. It then creates a runbook that you'll call from an MCP client.</p>
<h2>Run the MCP Server</h2>
<p>The easiest way to run the Octopus Easy Mode MCP server is to use the Docker image. The following <code>mcp.json</code> file can be used to configure the server:</p>
<pre><code class="language-json">{
  "servers": {
    "easymode": {
      "type": "stdio",
      "command": "docker",
      "args": [
        "run", 
        "--rm", 
        "-i",
        "--pull=always",
        "-e", "EASY_MODE_MCP_TRANSPORT=stdio", 
        "-e", "EASY_MODE_MCP_AUTH_TYPE=none", 
        "-e", "EASY_MODE_MCP_OCTOPUS_URL=https://yourinstance.octopus.app", 
        "-e", "EASY_MODE_MCP_OCTOPUS_API_KEY=API-APIKEYGOESHERE", 
        "-e", "EASY_MODE_MCP_OCTOPUS_SPACE_ID=Spaces-##", 
        "ghcr.io/octopussolutionsengineering/octopuseasymodemcp:latest"],
      "timeout": 600000
    }
  }
}
</code></pre>
<p>Replace the following values in the <code>mcp.json</code> file:</p>
<ul>
<li><code>EASY_MODE_MCP_OCTOPUS_URL</code> with the URL of your Octopus instance.</li>
<li><code>EASY_MODE_MCP_OCTOPUS_API_KEY</code> with an <a href="https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key">API key</a> that has access to the runbook you created.</li>
<li><code>EASY_MODE_MCP_OCTOPUS_SPACE_ID</code> with the ID of the space that contains the runbook you created.</li>
</ul>
<p>Now run the prompt from your chat client:</p>
<pre><code class="language-markdown">Get the current time
</code></pre>
<p>:::div{.hint}
Your MCP client may be able to return the date and time without needing to call an MCP server. If so, you can force the LLM to call the Easy Mode MCP server with the prompt <code>Get the current time from the easymode mcp server</code>.
:::</p>
<p>The MCP server will create a new snapshot of the runbook, run it, and return the result.</p>
<p>Importantly, you did not have to instruct the LLM to run a runbook. The Easy Mode MCP server does not require the end user to know that they are running runbooks, or even that the server is backed by Octopus. The runbooks are exposed directly as tools.</p>
<p>You can see this by asking the LLM to list the available tools from your chat client:</p>
<pre><code class="language-markdown">List the available tools
</code></pre>
<h2>Passing parameters to the tool</h2>
<p>Prompted variables are treated as parameters to the tool. For example, you can create a runbook that takes a name as a parameter and returns a greeting.</p>
<p>Run the following prompt in the Octopus AI Assistant:</p>
<pre><code class="language-markdown">Create a runbook called "Greet User" in the "Easy Mode MCP" project and then:
* Add a runbook description
* Add a single prompted variable called "Name" with the description "The name of the user to greet" and set the default value to "World".
* Scope the "Name" variable to the "Greet User" runbook.
* Add a single script step that echoes "Hello, #{Name}!".
* Configure the runbook to only run in the MCP environment.
</code></pre>
<p>Restart the MCP server to pick up the new tool, and then run the following prompt from your chat client:</p>
<pre><code class="language-markdown">Greet the user "Finn"
</code></pre>
<p>:::div{.hint}
The process of restarting the MCP server is different with each client. Visual Studio Code allows you to restart the MCP server by clicking the "Restart" link above the MCP server when editing the <code>mcp.json</code> file:</p>
<p><a href="/blog/img/mcp-easy-mode/vs-code-mcp-json.png">:img{ src="/blog/img/mcp-easy-mode/vs-code-mcp-json.png" alt="VSCode mcp.json" loading="lazy" }</a>
:::</p>
<p>The MCP client is smart enough to know the name <code>Finn</code> must be passed to the <code>Name</code> variable in the <code>Greet User</code> runbook, and the MCP server will return the greeting.</p>
<h2>Adding elicitation</h2>
<p><a href="https://modelcontextprotocol.io/specification/draft/client/elicitation">Elicitation</a> is the process of asking the user for information that is required to complete a task. The Octopus Easy Mode MCP server supports elicitation by adding manual intervention steps to a runbook.</p>
<p>Run the following prompt in the Octopus AI Assistant:</p>
<pre><code class="language-markdown">Create a runbook called "Welcome User" in the "Easy Mode MCP" project and then:
* Add a runbook description
* Add a single manual intervention step called "Ask for Name" with the instruction "Please enter your name" and the prompt "What is your name?".
* Add a single script step that echoes "Hello, #{Octopus.Action[Ask for Name].Output.Manual.Notes}!".
* Configure the runbook to only run in the MCP environment.
</code></pre>
<p>Restart the MCP server to pick up the new tool, and then run the following prompt from your chat client:</p>
<pre><code class="language-markdown">Welcome the user
</code></pre>
<p>You will be asked to enter your name and The MCP server will then return the greeting.</p>
<p>:::div{.warning}
Not all MCP clients support elicitation. This post was tested with Visual Studio Code and the GitHub Copilot Chat extension.
:::</p>
<p>You can automatically add generated notes to the manual intervention by setting <code>EASY_MODE_MCP_AUTO_POPULATE_INTERVENTION_NOTES</code> to <code>True</code>:</p>
<pre><code class="language-json">{
  "easymode": {
    "type": "stdio",
    "command": "docker",
    "args": ["run",
      "--rm",
      "-i",
      "--pull=always",
      "-e", "EASY_MODE_MCP_TRANSPORT=stdio",
      "-e", "EASY_MODE_MCP_AUTH_TYPE=none",
      "-e", "EASY_MODE_MCP_OCTOPUS_URL=https://yourinstance.octopus.app",
      "-e", "EASY_MODE_MCP_OCTOPUS_API_KEY=API-APIKEY",
      "-e", "EASY_MODE_MCP_OCTOPUS_SPACE_ID=Spaces-##",
      "-e", "EASY_MODE_MCP_AUTO_POPULATE_INTERVENTION_NOTES=True",
      "ghcr.io/octopussolutionsengineering/octopuseasymodemcp:latest"],
    "timeout": 600000
  }
}
</code></pre>
<p>The content placed into the notes section can be defined with the <code>EASY_MODE_MCP_AUTO_POPULATE_INTERVENTION_NOTES_VALUE</code> environment variable:</p>
<pre><code class="language-json">{
  "easymode": {
    "type": "stdio",
    "command": "docker",
    "args": ["run",
      "--rm",
      "-i",
      "--pull=always",
      "-e", "EASY_MODE_MCP_TRANSPORT=stdio",
      "-e", "EASY_MODE_MCP_AUTH_TYPE=none",
      "-e", "EASY_MODE_MCP_OCTOPUS_URL=https://yourinstance.octopus.app",
      "-e", "EASY_MODE_MCP_OCTOPUS_API_KEY=API-APIKEY",
      "-e", "EASY_MODE_MCP_OCTOPUS_SPACE_ID=Spaces-##",
      "-e", "EASY_MODE_MCP_AUTO_POPULATE_INTERVENTION_NOTES=True",
      "-e", "EASY_MODE_MCP_AUTO_POPULATE_INTERVENTION_NOTES_VALUE=Your custom note",
      "ghcr.io/octopussolutionsengineering/octopuseasymodemcp:latest"],
    "timeout": 600000
  }
}
</code></pre>
<p>:::div{.hint}
Setting the <code>EASY_MODE_MCP_AUTO_POPULATE_INTERVENTION_NOTES</code> environment variable to <code>True</code> allows you to run runbooks that contain manual intervention steps without any user interaction. This is useful for MCP clients that do not support elicitation or for automating processes that require manual intervention.
:::</p>
<h2>Practical examples</h2>
<p>Here are some practical Runbooks that you might expose to an MCP server.</p>
<h3>Creating cloud resources</h3>
<p>A common scenario for DevOps teams is to provision new cloud resources. This is often done with a combination of Terraform and Octopus.</p>
<p>Run the following prompt in the Octopus AI Assistant:</p>
<pre><code class="language-markdown">Create a runbook called "Create EC2 Instance" in the "Easy Mode MCP" project and then:
* Create a feed called "Docker Hub" pointing to "https://index.docker.io" using anonymous authentication.
* Add a runbook description
* Add a Terraform Apply step to the runbook called "Create EC2 Instance" with the following configuration:
```
# A mocked Terraform configuration simulating the construction of an EC2 instance
output "ec2_instance_id" {
  value = "i-1234567890abcdef0"
}
```
* Use the "Hosted Ubuntu" worker pool.
* Configure the Terraform step to use the execution container image "octopusdeploy/worker-tools:6.6.4-ubuntu.22.04" from the "Docker Hub" feed
* Add a step to run a script that writes `Your instance is #{Octopus.Action[Apply a Terraform template].Output.TerraformValueOutputs[ec2_instance_id]}` as a highlight.
* Configure the runbook to only run in the MCP environment.
* The runbook must be untenanted.
</code></pre>
<p>Restart the MCP server to pick up the new tool, and then run the following prompt from your chat client:</p>
<pre><code class="language-markdown">Create a new EC2 instance
</code></pre>
<h3>Debugging a Kubernetes application</h3>
<p>In this example, we'll imagine that a support team needs to restart a Kubernetes application. The runbook will delete the pod, triggering a restart.</p>
<p>Run the following prompt in the Octopus AI Assistant:</p>
<pre><code class="language-markdown">Create a runbook called "Restart K8s Web App" in the "Easy Mode MCP" project and then:
* Define the first step as a kubectl script step to the runbook called "Restart K8s Web App"
* Use the "Hosted Ubuntu" worker pool.
* Configure the kubectl step to use the execution container image "octopusdeploy/worker-tools:6.6.4-ubuntu.22.04" from the "Docker Hub" feed
* Add a script that checks for a pod called "my-web-app-pod", and if it exists, deletes it. If the pod doesn't exist, the script should write a message to the log and exit successfully.
* Use client side apply in the Kubernetes step (the mock Kubernetes cluster only supports client side apply).
* Disable verification checks in the Kubernetes steps (the mock Kubernetes cluster doesn't support verification checks).
* Enable retries on the K8s deployment step.
* Define the second step as a Slack notification step to the runbook called "Notify Slack" that sends a message to the channel "#k8s-notifications" with the message "The K8s Web App has been restarted." using the web hook url "https://mockslackwebhook.octopusdemos.com/".
* Configure the runbook to only run in the MCP environment.
* The runbook must be untenanted.

---

Create a token account called "Mock Token".

---

Create a feed called "Docker Hub" pointing to "https://index.docker.io" using anonymous authentication.

---

Create a Kubernetes target 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, and the "Hosted Ubuntu" worker pool. Scope the target to the "MCP" environment.
</code></pre>
<p>Restart the MCP server to pick up the new tool, and then run the following prompt from your chat client:</p>
<pre><code class="language-markdown">Restart the Kubernetes Web App
</code></pre>
<p>What is neat about this example is that we have captured a lot of business logic in the runbook. The pod to be deleted is defined, eliminating the need for the end user to know which pod to delete. The Slack notification step is also defined, so the team is notified when the pod is restarted.</p>
<p>While this logic could be captured in an LLM skill, you can imagine how many tokens would be required for the LLM to reason about the steps to restart a Kubernetes application and send a Slack notification. By exposing the process as a deterministic set of steps in a runbook, the LLM only needs to know that a tool exists to restart the Kubernetes application, and it can delegate execution of that process to the Octopus server.</p>
<h3>Bootstrapping a new project</h3>
<p>Here is an example where we create a runbook to call the AI Assistant to create a new Terraform project. The <a href="https://library.octopus.com/step-templates/8ce3eb55-2c35-45c2-be8c-27e71ffbf032/actiontemplate-octopus-prompt-ai">Octopus - Prompt AI</a> step allows you to run the same prompts you have been typing into the AI Assistant directly from a runbook.</p>
<p>Run the following prompt in the Octopus AI Assistant:</p>
<pre><code class="language-markdown">Create a runbook called "Create new Terraform project" in the "Easy Mode MCP" project and then:
* Define the first step as an "Octopus - Prompt AI" step
* Set the prompt to "Create a new Terraform project with the following name: #{ProjectName} in the #{Octopus.Space.Name} space." and enable auto approve.
* Add a prompted variable called "ProjectName" with the description "The name of the new Terraform project" and scope it to the "Create new Terraform project" runbook.
* Configure the runbook to only run in the MCP environment.
* The runbook must be untenanted.
</code></pre>
<p>You will need to <a href="https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key">create an API key</a> for the "Octopus - Prompt AI" step and define it in the <code>Project.Octopus.Api.Key</code> variable.</p>
<p>Restart the MCP server to pick up the new tool, and then run the following prompt from your chat client:</p>
<pre><code class="language-markdown">Create a new Terraform project called "My New Project"
</code></pre>
<p>A new project will be created in the same space as the runbook to deploy a sample Terraform configuration.</p>
<p>You're now chaining AI agents:</p>
<ol>
<li>The MCP client calls the Easy Mode MCP server</li>
<li>The Easy Mode MCP server calls a runbook</li>
<li>The runbook calls the AI Assistant to create a new Terraform project</li>
<li>The AI Assistant calls an LLM to create a new Terraform project</li>
</ol>
<h2>Governance and compliance</h2>
<p>Because the Easy Mode MCP server is backed by Octopus, it inherits all the governance and compliance features of Octopus.</p>
<p>You get audit logs showing what was run, when, and a persistent log of all the output:</p>
<p><a href="/blog/img/mcp-easy-mode/audit-logs.png">:img{ src="/blog/img/mcp-easy-mode/audit-logs.png" alt="Audit Logs" loading="lazy" }</a></p>
<p>These logs can optionally be <a href="https://octopus.com/docs/security/users-and-teams/auditing/audit-stream">sent to an external log aggregation service</a>, such as Splunk or Datadog, for long-term retention and analysis.</p>
<p>The runbooks can be subject to <a href="https://octopus.com/docs/platform-hub/policies">Platform Hub policies</a>, and can consume <a href="https://octopus.com/docs/platform-hub/templates/process-templates">process templates</a>.</p>
<p>Runbook events can trigger <a href="https://octopus.com/docs/administration/managing-infrastructure/subscriptions/webhook-slack#configure-an-octopus-subscription-to-send-a-webhook">webhooks</a> to notify external systems of runbook execution.</p>
<p>All credentials are centrally managed and can be sourced from external secret management systems, such as <a href="https://octopus.com/blog/using-hashicorp-vault-with-octopus-deploy">HashiCorp Vault</a>.</p>
<p>Octopus provides a robust platform with proven governance and compliance features, all of which are now available to AI agents through the Easy Mode MCP server.</p>
<h2>Difference between the Easy Mode MCP server and the Octopus MCP server</h2>
<p><a href="https://octopus.com/docs/octopus-ai/mcp">Octopus provides a general purpose MCP server</a> that allows MCP clients to execute common Octopus operations, such as creating releases, deploying releases, running runbooks, getting deployment logs, etc.</p>
<p>The Octopus MCP server is more than capable of running runbooks, but it does not inherently know about the existence of runbooks, nor does it have a reason to link a runbook to a specific task.</p>
<p>For example, to run the "Get Current Time" runbook, you would write a prompt like this:</p>
<pre><code class="language-markdown">Run the runbook "Get Current Time" in the "Easy Mode MCP" project in the "MCP" environment in the "Default" space.
</code></pre>
<p>This command queries the space to get the space ID, the project to get the project ID, and the runbook to get the runbook ID. It then runs the runbook and returns the result. It then executes the runbook and returns the result.</p>
<p>In Claude Code, this was the token usage:</p>
<pre><code class="language-text">Usage by model:
    claude-haiku-4-5:  549 input, 17 output, 0 cache read, 0 cache write ($0.0006)
       claude-opus-5:  16 input, 1.5k output, 157.8k cache read, 12.7k cache write ($0.1956)
</code></pre>
<p>Running the same runbook with the Easy Mode MCP server, the token count is significantly lower:</p>
<pre><code class="language-text">Usage by model:
    claude-haiku-4-5:  521 input, 14 output, 0 cache read, 0 cache write ($0.0006)
       claude-opus-5:  4 input, 145 output, 29.0k cache read, 5.0k cache write ($0.0497)
</code></pre>
<p>By directly exposing runbooks as tools, the Easy Mode MCP server provides a more efficient way for LLMs to execute runbooks, reducing token usage and improving performance.</p>
<h2>Conclusion</h2>
<p>The Easy Mode MCP server provides a simple way for AI agents and MCP clients to execute Octopus runbooks as tools. This means AI-based workflows gain the scale, reliability, auditability, governance, and convenience of Octopus. And because most of the work is performed by Octopus, AI agents reduce their token use by offloading the execution of complex processes to Octopus.</p>]]></content>
    </entry>
    <entry>
      <title>Connecting Octopus Cloud to your internal systems — without opening a single port</title>
      <link href="https://octopus.com/blog/octopus-cloud-connection-agent" />
      <id>https://octopus.com/blog/octopus-cloud-connection-agent</id>
      <published>2026-08-03</published>
      <updated>2026-08-03</updated>
      <summary>The Octopus Connection Agent lets enterprises reach their on-premises tooling from Octopus Cloud securely, with no inbound firewall rules and no VPN.</summary>
      <author>
        <name>Mark Lamprecht, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>For many enterprise teams, the move to Octopus Cloud can run into the same limitation: their artifact repositories, source control servers, and other internal tooling live behind a network perimeter that was never designed to accept connections from the outside world. Opening inbound firewall ports isn't an option—not when security and compliance teams have spent considerable effort locking those down.</p>
<p>So teams make a pragmatic choice: stay on Octopus Server and deal with the cloud migration later. But by extension then they continue to manage Octopus upgrades and operating system patching too; and have to accept they won't have access to new Octopus features as they come out.</p>
<p>We've watched this play out with a number of large enterprises. One of them is a major global financial services institution in the middle of a significant undertaking: migrating over a decade's worth of legacy deployment infrastructure to Octopus. Thousands of pipelines and years of accumulated configuration. A migration that is touching nearly every team in the organization.</p>
<p>Going straight to Octopus Cloud is the obvious goal — less infrastructure to manage, no platform to maintain. But it's being blocked by a straightforward problem: Octopus Cloud has no way to reach the systems that matter most—their internal Artifactory instance, and their GitHub Enterprise server and other internal systems. In a regulated environment, asking the security team to open inbound ports to a third-party SaaS platform isn't a conversation that team wants to have.</p>
<p>Enter the <strong>Octopus Connection Agent</strong>, available from July 2026.</p>
<h2>How it works</h2>
<p>The Connection Agent is a lightweight Docker container you run inside your own network. When it starts, it opens a secure, outbound-only connection to your Octopus Cloud instance over port 443—the same port used for standard HTTPS traffic.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/connection-agent-architecture/img-connection-agent-architecture.png" alt="Connection Agent architecture" loading="lazy" }</p>
<p>:::</p>
<p>Because the connection is initiated from inside your network, nothing needs to change on your firewall. No inbound rules. No VPN. No publicly accessible endpoints for your internal systems. The agent authenticates using a private key, and Octopus proxies traffic through it to reach whatever internal resources you need.</p>
<p>The financial services institution in this story ran the Connection Agent as a Kubernetes pod inside their own cluster—fitting naturally into the container infrastructure they already operated. After completing the registration step, the logs confirmed what they'd been waiting for:</p>
<pre><code class="language-text">[10:55:05 INF] Registering Connection Agent with Octopus instance URL: https://{instance}.octopus.app/api/connectionagent/registrations
[10:55:05 INF] Successfully registered Connection Agent
</code></pre>
<p>Within a single maintenance window, their Octopus Cloud instance was reprovisioned, and traffic began flowing through the agent to their internal GitHub Enterprise server. Artifactory followed shortly after. The only firewall edit required: outbound port 443 access to their Octopus Cloud instance and to Octopus's authentication service.</p>
<h2>From blocker to enabler</h2>
<p>Once the Connection Agent is running, they can get on with populating Octopus Cloud, skipping an entire phase of infrastructure work—requesting and configuring firewall ports—and instead focusing on what actually matters, getting thousands of pipelines migrated and teams deploying to production.</p>
<p>That's not a small thing as every migration carries risk. And adding yet more company infrastructure configuration steps into the mix—in this case extra firewall rules—doesn't actually move the migration forwards, it only further compounds the risk. By using the Connection Agent, the company removes that risk entirely.</p>
<h2>Security by design</h2>
<p>For teams in regulated industries, security isn't a checkbox—it's a constraint that shapes every decision. The Connection Agent was designed with that in mind.</p>
<p>All connections are outbound-only. The agent initiates the connection; Octopus never reaches into your network. Authentication uses short-lived JWT access tokens signed by 256-bit RSA private keys. You can configure <code>ALLOWED_IP_RANGES</code> to lock down exactly which internal hosts the agent can access, so you define the boundary explicitly rather than leaving it open-ended.</p>
<p>At this early stage, we support the publicly accessible Certificate Authorities (CAs). Though if your internal systems use certificates issued by an internal certificate authority—common in financial services—then for now you can use those internal certificates and CAs by setting <code>--ignore-certificate-validation-errors</code> for a given internal domain.</p>
<p>So if you need to pin specific CAs or certificates, let us know!</p>
<p>What's more, if you need redundancy, you can run multiple agents across different Kubernetes clusters.</p>
<h2>What's supported</h2>
<p>The Connection Agent currently supports:</p>
<ul>
<li>Git repositories (e.g. GitHub Enterprise, Bitbucket)</li>
<li>SMTP servers</li>
<li>External feeds:
<ul>
<li>Artifactory Generic Feed</li>
<li>Azure Container Registry</li>
<li>Docker Container Registry</li>
<li>GitHub Repository Feed</li>
<li>Helm Feed</li>
<li>Maven Feed</li>
<li>NPM Feed</li>
<li>NuGet Feed</li>
<li>OCI Container Registry</li>
</ul>
</li>
</ul>
<p>Support for additional resource types is in active development. Early adopters are working directly with the team to shape what comes next, which means real customer use cases are driving the roadmap.</p>
<p><strong>Action:</strong> We would love to hear what other connectors would help. You can <a href="https://roadmap.octopus.com/c/220-connect-to-self-hosted-applications-from-octopus-cloud">share them with us here</a>.</p>
<h2>Getting started</h2>
<p>The Connection Agent is available as a Docker image on <a href="https://hub.docker.com/r/octopusdeploy/connection-agent/tags">Docker Hub</a> and is limited to a maximum of 5 connections i.e. 5 internal resources as of this writing.</p>
<p>Full documentation <a href="https://octopus.com/docs/octopus-cloud/connection-agent">can be found here</a>.</p>
<p>If you're on Octopus Server and a connectivity gap is what's been keeping you from moving to cloud, or if you're already on Octopus Cloud and working around the lack of access to your internal systems, talk to your account team as this is the piece that was missing.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Easy Mode - Claude Agent</title>
      <link href="https://octopus.com/blog/octo-easy-mode-17-claude" />
      <id>https://octopus.com/blog/octo-easy-mode-17-claude</id>
      <published>2026-07-31</published>
      <updated>2026-07-31</updated>
      <summary>Learn how to create a Claude Agent project</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>AI is driving a surge in code commits. <a href="https://x.com/kdaigle/status/2040164759836778878">Kyle Daigle, COO at GitHub, noted that</a>:</p>
<blockquote>
<p>There were 1 billion commits in 2025. Now, it's 275 million per week, on pace for 14 billion this year if growth remains linear (spoiler: it won't.)</p>
</blockquote>
<p>Of course, commits are only useful if they contribute to running software. The challenge for many teams is increasing deployment frequency to match commit frequency. Ideally, changes with limited risk should be deployed with as little friction as possible. But how do you determine something as abstract as a low-risk commit?</p>
<p>Code is just text, and LLMs are incredible at comprehending text. With the new <a href="https://octopus.com/docs/octopus-ai/claude-agent-step">Run Claude Agent</a> step in Octopus, combined with <a href="https://octopus.com/docs/packaging-applications/build-servers/build-information">Build Information</a>, Octopus can automatically inspect commits that contribute to a deployment and make intelligent decisions at deploy time based on the commit content.</p>
<p>In the <a href="/blog/octo-easy-mode-16-argocd-manifest-update">previous post</a>, you created a project simulating updating an <a href="https://octopus.com/docs/argo-cd/steps/update-application-manifests">Argo CD Manifest file</a>.</p>
<p>In this post, you will create a sample project that uses the <code>Run Claude Agent</code> step to determine if a commit is low risk and can be deployed automatically.</p>
<p><a href="https://samples.octopus.app/app#/Spaces-1213/projects/categorize-changes">Live Demo</a></p>
<h2>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>
<p>:::div{.hint}
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>
<h2>Creating the project</h2>
<p>Paste the following prompt into the Octopus AI Assistant and run it to create a sample project using the <code>Run Claude Agent</code> step to categorize commits:</p>
<pre><code class="language-markdown">Create a Claude project called "17. Categorize Changes"
</code></pre>
<p>The resulting project uses Build Information to associate commits with a package included in the deployment.</p>
<p>You must provide two API keys for this project to work:</p>
<ol>
<li>A GitHub Personal Access Token (PAT) with <code>repo</code> scope saved in the <code>Project.GitHub.PAT</code> project variable</li>
<li>A Claude API Key saved in the <code>Project.Claude.ApiKey</code> project variable</li>
</ol>
<p>The Claude step also requires the <code>claude</code> CLI to be installed on the worker. This can be provided by an <a href="https://octopus.com/docs/projects/steps/execution-containers-for-workers">Execution Container Image</a> with the following inline <code>Dockerfile</code>:</p>
<pre><code class="language-Dockerfile">FROM python:3.11-slim

# 1. Install prerequisites (including libicu for .NET Calamari compatibility)
RUN apt-get update &#x26;&#x26; apt-get install -y --no-install-recommends \
    curl \
    git \
    libicu-dev \
    ca-certificates \
    &#x26;&#x26; rm -rf /var/lib/apt/lists/*

# 2. Install the native Claude Code CLI tool 
RUN curl -fsSL https://claude.ai/install.sh | bash

# 3. Create a global symlink using the exact path from your install log
# This bypasses the $PATH profile restriction for non-interactive runners
RUN ln -sf /root/.local/bin/claude /usr/local/bin/claude

WORKDIR /app
CMD ["/bin/bash"]
</code></pre>
<p><a href="/blog/img/octo-easy-mode-17-claude/inline-docker.png">:img{ src="/blog/img/octo-easy-mode-17-claude/inline-docker.png" alt="Inline Docker file" loading="lazy" }</a></p>
<h2>Categorizing commits</h2>
<p>The prompt defined in the <code>Run Claude Agent</code> step is designed to categorize commits:</p>
<pre><code class="language-markdown">Your task is to rate the impact of the Git commits that contribute to the new version of the application being deployed.

The following is the list of Git commits:

#{each change in Octopus.Deployment.Changes}
#{each commit in change.Commits}
#{commit.LinkUrl}
#{/each}
#{/each}

Output a value between 1 and 10 based on the impact of the changes in the following categories:

* Security
* User Interface
* Documentation
* Business Logic
* Performance
* Code dependencies
* Code refactoring

The result must be a plain JSON blob like this:

```
{
"security": 1,
"userInterface": 4,
"documentation": 7,
"businessLogic": 3,
"performance": 1,
"dependencies": 9,
"refactoring": 5
}
```
</code></pre>
<p>The key to linking Build Information with the <code>Run Claude Agent</code> step is the <code>Octopus.Deployment.Changes</code> variable. You loop over every change, then over every commit in that change. The <code>LinkUrl</code> property is used to embed a link to the commit in the prompt.</p>
<p>You then use the GitHub MCP server to access the commit content:</p>
<p><a href="/blog/img/octo-easy-mode-17-claude/github-mcp.png">:img{ src="/blog/img/octo-easy-mode-17-claude/github-mcp.png" alt="GitHub MCP Server" loading="lazy" }</a></p>
<p>:::div{.hint}
It is best practice to use MCP servers to interact with external services over general CLI tools like <code>curl</code>. The tools exposed by MCP servers have limited scope, are tested, and are constrained by the provided credentials. General tools like <code>curl</code> can initiate literally any web request, and LLMs will often go to great lengths constructing web requests to achieve their goals.</p>
<p>The step also includes a range of <a href="https://octopus.com/docs/octopus-ai/claude-agent-step/security-and-compliance">security and compliance features</a> to restrict the agent.
:::</p>
<h2>Demonstrating a low-risk change</h2>
<p>Start by pushing a Build Information package to Octopus that links to a number of low-risk commits. Save the JSON blob below to a file called <code>buildinfo.json</code>:</p>
<pre><code class="language-json">{
  "BuildEnvironment": "GitHub Actions",
  "Branch": "main",
  "BuildNumber": "658",
  "BuildUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/actions/runs/29776705931",
  "VcsType": "Git",
  "VcsRoot": "https://github.com/OctopusSolutionsEngineering/Octopub",
  "VcsCommitNumber": "a84a77fd046e329bb10405480486ce9c11db6074",
  "Commits": [
    {
      "Id": "a84a77fd046e329bb10405480486ce9c11db6074",
      "LinkUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/commit/a84a77fd046e329bb10405480486ce9c11db6074",
      "Comment": "Accidentally improved the intern with zero tests"
    },
    {
      "Id": "bb8754a163ec1b278c4e2a3e31bcb868d5d0eb70",
      "LinkUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/commit/bb8754a163ec1b278c4e2a3e31bcb868d5d0eb70",
      "Comment": "Accidentally improved Schrödinger's bug against my better judgment"
    },
    {
      "Id": "1e9e34564e7e5352e4fc168377f5dd2585a6069f",
      "LinkUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/commit/1e9e34564e7e5352e4fc168377f5dd2585a6069f",
      "Comment": "Stared menacingly at the dark arts with zero tests"
    }
  ]
}
</code></pre>
<p>If you open the links in the <code>LinkUrl</code> fields, you will see that these commits are gibberish changes to a text file. They are low risk because they don't change any code that is executed in the application.</p>
<p>Push the Build Information package to Octopus using the <code>octopus</code> CLI, replacing the space name with your actual space name.</p>
<p>This is the Bash command:</p>
<pre><code class="language-bash">octopus build-information upload \
  --space "Your Space Name" \
  --package-id "com.octopus:octopub-frontend" \
  --version "20260721.659.1" \
  --file "buildinfo.json" \
  --overwrite-mode "overwrite"
</code></pre>
<p>This is the PowerShell command:</p>
<pre><code class="language-powershell">octopus build-information upload `
  --space "Your Space Name" `
  --package-id "com.octopus:octopub-frontend" `
  --version "20260721.659.1" `
  --file "buildinfo.json" `
  --overwrite-mode "overwrite"
</code></pre>
<p>:::div{.hint}
In a production scenario, you would typically automate the generation of Build Information as part of your CI/CD pipeline. This ensures that the information is always up to date and accurately reflects the state of your codebase.</p>
<p>For the purposes of this demonstration, we are manually constructing and pushing mock Build Information to Octopus to simulate different commit scenarios.
:::</p>
<p>When you deploy a release of the project, the steps will:</p>
<ol>
<li>Print a list of the commit links</li>
<li>Run the <code>Run Claude Agent</code> step to categorize the commits</li>
<li>Extract the JSON blob generated by the Claude agent, and determine if any high-risk categories have a value greater than 5</li>
<li>If any high-risk categories are detected, a manual intervention step will be triggered, requiring a human to approve the deployment</li>
<li>If all categories are low risk, the manual intervention step will be skipped</li>
<li>Proceed to a mock deployment step that simulates deploying the application</li>
</ol>
<p>Since the commits in this example are low risk, each category gets a low score:</p>
<p><a href="/blog/img/octo-easy-mode-17-claude/claude-output.png">:img{ src="/blog/img/octo-easy-mode-17-claude/claude-output.png" alt="Claude Output" loading="lazy" }</a></p>
<p>Based on these scores, the manual intervention step is skipped, and the deployment proceeds automatically.</p>
<h2>Demonstrating a high-risk change</h2>
<p>Save the following code to a file called <code>buildinfo.json</code>:</p>
<pre><code class="language-json">{
  "BuildEnvironment": "GitHub Actions",
  "Branch": "main",
  "BuildNumber": "658",
  "BuildUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/actions/runs/29776705931",
  "VcsType": "Git",
  "VcsRoot": "https://github.com/OctopusSolutionsEngineering/Octopub",
  "VcsCommitNumber": "982860ff9295c75ea3f3f0f963b09f0db3138e4e",
  "Commits": [
    {
      "Id": "7ec4ef85a5aac729b2d3e823307cb4c4caa63d58",
      "LinkUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/commit/7ec4ef85a5aac729b2d3e823307cb4c4caa63d58",
      "Comment": "Enhance URL safety and update footer links for improved security"
    },
    {
      "Id": "982860ff9295c75ea3f3f0f963b09f0db3138e4e",
      "LinkUrl": "https://github.com/OctopusSolutionsEngineering/Octopub/commit/982860ff9295c75ea3f3f0f963b09f0db3138e4e",
      "Comment": "Update dependencies and enhance project configuration for improved compatibility"
    }
  ]
}
</code></pre>
<p>Push the new Build Information package to Octopus using the <code>octopus</code> CLI commands provided earlier, replacing the space name with your actual space name.</p>
<p>These commits represent realistic changes that update dependencies and improve security. When you deploy a release of the project with this Build Information, the <code>Run Claude Agent</code> step will categorize the commits and produce a JSON blob with higher scores in the security and dependencies categories:</p>
<p><a href="/blog/img/octo-easy-mode-17-claude/claude-output-high-risk.png">:img{ src="/blog/img/octo-easy-mode-17-claude/claude-output-high-risk.png" alt="Claude Output High Risk" loading="lazy" }</a></p>
<p>These score values indicate that the changes are high risk, and the manual intervention step will be triggered, requiring a human to approve the deployment before it can proceed.</p>
<h2>What just happened?</h2>
<p>You created a sample project with:</p>
<ul>
<li>Associated Build Information that links to commits in a GitHub repository</li>
<li>A <code>Run Claude Agent</code> step that categorizes commits based on their impact</li>
<li>A step that parses the JSON output from the Claude agent and determines if any high-risk categories are present</li>
<li>A manual intervention step that is conditionally triggered based on the risk assessment</li>
</ul>
<h2>What's next?</h2>
<p>The <a href="/blog/octo-easy-mode-18-progressive-rollouts">next step</a> is an example of progressive rollouts through multiple production environments.</p>]]></content>
    </entry>
    <entry>
      <title>How to promote a release from Development to Production With Argo CD and Octopus Deploy</title>
      <link href="https://octopus.com/blog/promote-release-with-argo-cd-and-octopus" />
      <id>https://octopus.com/blog/promote-release-with-argo-cd-and-octopus</id>
      <published>2026-07-30</published>
      <updated>2026-07-30</updated>
      <summary>Connect Argo CD to Octopus Deploy and promote a single, immutable release from Development to Production with approval gates and a full audit trail.</summary>
      <author>
        <name>Jubril Oyetunji, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In vanilla Argo CD, "promoting to production" is really just editing a YAML file in a different folder and hoping you got it right. You bump an image tag in a production overlay, commit, and trust that what you just wrote matches what you verified in Development.</p>
<p>This is great until an auditor asks, "Who promoted this, and when?" or an incident traces back to a tag nobody meant to change.</p>
<p>Argo CD is excellent at keeping a cluster in sync with Git, but it has no concept of a release, i.e, no single, frozen artifact that moves from one environment to the next under policy.</p>
<p>In this guide, you will connect Argo CD to Octopus Deploy and turn promotion into a governed release, using the same immutable snapshot to move from Development to Production, gated by approval.</p>
<p>The Audit Stream and connection reuse the setup from our <a href="https://octopus.com/blog/connecting-aws-eks-argo-cd-to-octopus-cloud">EKS connection walkthrough</a>, so this article stays focused on promotion.</p>
<h2>Why "promotion" is hard in vanilla Argo CD</h2>
<p>Argo CD treats each Application as an independent unit. The dev install of your app and the production install are two separate Applications with no codified relationship between them. Nothing in Argo CD knows that "web in production" should receive exactly what "web in dev" was verified with.</p>
<p>Similarly, depending on your organization or team, promoting to an environment could mean a separate namespace or an entirely new cluster, both of which Octopus Deploy can handle.</p>
<p>That fragmented trail is slow and painful to reassemble at exactly the moments you need it most. Like when an auditor asks who promoted what and when, or when you are mid-incident trying to work out what changed.</p>
<p>Whereas, what you want is a single, frozen release that moves through environments under governance: verified once in Development, promoted unchanged to Production, with the who and when captured automatically.</p>
<p>That leaves two do-it-yourself options for promotion, and both are ad-hoc:</p>
<ul>
<li><strong>Hand-edit the image tag</strong> in each environment's overlay folder, commit, and let Argo sync. This is fast, but there is no record of intent, no gate, and nothing stopping a typo from shipping a different tag to Production than the one you tested.</li>
<li><strong>Script a pull request per environment.</strong> This is more controlled, but now your promotion logic lives in CI YAML and shell, reinvented per team, drifting as the estate grows.</li>
</ul>
<p>Whereas, what you want is a single, frozen release that moves through environments under governance: verified once in Development, promoted unchanged to Production, with the who and when captured automatically.</p>
<h2>Prerequisites</h2>
<p>This walkthrough builds on the cluster and Octopus connection from the <a href="https://octopus.com/blog/connecting-aws-eks-argo-cd-to-octopus-cloud">EKS connection post</a>. You do not need EKS specifically, but you do need these pieces in place before the promotion steps make sense:</p>
<ul>
<li><strong>An Octopus Deploy instance with the Argo CD integration</strong> (Octopus Cloud or self-hosted). This is where the project, lifecycle, and release live.</li>
<li><strong>A Kubernetes cluster you can install into.</strong> A local <a href="https://kind.sigs.k8s.io/">kind</a> cluster is enough. Because the Octopus gateway dials outbound, no ingress or public address is required.</li>
<li><strong>Argo CD running in that cluster, connected to Octopus through the gateway.</strong> If you followed the EKS connection post, reuse that same cluster and its gateway connection. If you are starting fresh, the next section installs Argo CD and registers the gateway from scratch.</li>
<li><strong><code>kubectl</code>, <code>helm</code>, and the <code>argocd</code> CLI</strong> installed locally.</li>
<li><strong>A Git repository for your manifests</strong> with Kustomize overlays per environment (the demo uses a public GitHub repo), plus a Git credential in Octopus that can push to it.</li>
</ul>
<h2>The architecture setup</h2>
<p>For this demo, we're aiming for a single Kubernetes cluster with two namespaces that serve as environments, <code>dev</code> and <code>production</code>, each with its own Argo CD Application.</p>
<p>Octopus owns the release and promotion process, and Git remains the source of truth, while Argo CD applies manifests to the cluster.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/architecture.png" alt="How Octopus, Git, and Argo CD interact through commits." loading="lazy" }</p>
<p>::figcaption[Octopus commits the new image tag to the right overlay and triggers a sync through an in-cluster gateway. Argo CD pulls from Git and reconciles each namespace and Octopus never needs inbound access to your cluster.]</p>
<p>:::</p>
<p>The <strong>Octopus gateway</strong> is a small component you install in the cluster with Helm; it dials <strong>outbound</strong> to Octopus over gRPC, so nothing in your cluster needs a public address. That means this entire demo can run on a local <a href="https://kind.sigs.k8s.io/">kind</a> cluster with no ingress.</p>
<p>For an in-depth look at the cluster and gateway connection, see the <a href="https://octopus.com/blog/connecting-aws-eks-argo-cd-to-octopus-cloud">EKS connection post</a>; here, we install Argo CD, register the gateway, and proceed to promotion.</p>
<p>Install Argo CD with a dedicated <code>octopus</code> account so the gateway has its own scoped identity rather than piggybacking on <code>admin</code>:</p>
<pre><code class="language-bash">helm install argocd argo-cd \
 --repo https://argoproj.github.io/argo-helm \
  --create-namespace --namespace argocd --wait --timeout 10m \
 --values - &#x3C;&#x3C; 'EOF'
configs:
  cm:
    accounts.octopus: apiKey
  rbac:
    policy.default: "role:readonly"
    policy.csv: |
      g, admin, role:admin
      p, octopus, applications, get, *, allow
      p, octopus, applications, sync, *, allow
      p, octopus, clusters, get, *, allow
      p, octopus, logs, get, */*, allow
EOF
</code></pre>
<p>With Argo CD running, register the instance in Octopus (<strong>Infrastructure</strong>, then <strong>Argo CD Instances</strong>, then <strong>Add Argo CD Instance</strong>), paste an auth token for the <code>octopus</code> account, and Octopus generates a Helm command for the gateway.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/register-argo-instance.png" alt="Register an Argo CD instance" loading="lazy" }</p>
<p>::figcaption[Registering the Argo CD instance. The service DNS name is the in-cluster address of the Argo CD API server.]</p>
<p>:::</p>
<p>Run the generated Helm command against your cluster, and Octopus confirms the connection: the gateway registers, connects to Octopus, and connects to Argo CD.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/install-gateway.png" alt="Install gateway" loading="lazy" }</p>
<p>::figcaption[The gateway bridges Octopus and Argo CD over an outbound connection. No inbound firewall rules required.]</p>
<p>:::</p>
<p><em>The gateway bridges Octopus and Argo CD over an outbound connection. No inbound firewall rules required.</em></p>
<h3>Map the Applications with annotations</h3>
<p>Octopus needs to know which Argo CD Applications belong to which project and environment. You declare that with two annotations on each Application manifest. No per-application configuration is needed in Octopus; the annotations handle the mapping.</p>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-dev
  namespace: argocd
  annotations:
    argo.octopus.com/project: argo-web-promotion
    argo.octopus.com/environment: development
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-web-promotion
    targetRevision: main
    path: overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: dev
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [ CreateNamespace=true ]
</code></pre>
<p>The <code>argo.octopus.com/project</code> annotation ties the Application to the Octopus project, and <code>argo.octopus.com/environment</code> ties it to an Octopus environment. The production Application is identical except <code>name: web-production</code>, <code>argo.octopus.com/environment: production</code>, <code>path: overlays/production</code>, and <code>namespace: production</code>.</p>
<p>When Octopus deploys <code>argo-web-promotion</code> to Development, it now knows <code>web-dev</code> is the Application to update; when it deploys to Production, it updates <code>web-production</code>.</p>
<p>Both overlays are simple Kustomize folders that set the image tag. This is the field Octopus will rewrite:</p>
<pre><code class="language-yaml"># overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
 - ../../base
images:
 - name: nginx
    newTag: "1.27.0"
</code></pre>
<h2>Building the Octopus project with a Dev to Production lifecycle</h2>
<p>Create an Octopus project and give it a lifecycle with two phases, Development and Production. The lifecycle is what makes promotion ordered, which simply means a release must pass through Development before it can reach Production.</p>
<p>Then add the built-in <strong>Update Argo CD Application Image Tags</strong> step to the deployment process. For each Application matched by annotation, this step retrieves the Git location from the Application, updates the image tag in the manifests, commits the change, and triggers Argo CD to sync. Add a container image reference (the <code>nginx</code> image, from a Docker Hub feed) so the release knows which image to update and what version to pin.</p>
<p>To make the governance visible, add one more step before it: a <strong>Manual intervention</strong> step scoped to the Production environment only. That is your approval gate. It runs when promoting to Production and is skipped for Development, so it stays fast while Production stays governed.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/argo-web-promotion.png" alt="Argo web promotion" loading="lazy" }</p>
<p>::figcaption[Two steps, one governed process. The approval runs only for Production; the image-tag update runs for any environment.]</p>
<p>:::</p>
<h2>Create a release and deploy to Development</h2>
<p>Create a release in Octopus and select the image version to promote, for example <code>nginx:1.27.2</code> (a bump from the <code>1.27.0</code> currently in the overlays). This release is a frozen snapshot of the process, variables, and package versions. Once created, it is immutable: the version that goes to Production later is the exact version you are about to verify in Development, not whatever happens to sit at Git HEAD.</p>
<p>Deploy the release to Development. Octopus commits the new tag to the dev overlay, and Argo CD syncs the <code>dev</code> namespace:</p>
<pre><code class="language-bash">Credential 'gitops-web-promotion' will be used to access the repository
Committing directly to branch for changes in this environment
Cloning repository https://github.com/your-org/gitops-web-promotion
</code></pre>
<p>Within seconds, the dev Application is synced and Healthy on the new tag, while Production is untouched:</p>
<pre><code class="language-bash">$ kubectl get deploy web -n dev -o jsonpath='{..image}'
nginx:1.27.2
$ kubectl get deploy web -n production -o jsonpath='{..image}'
nginx:1.27.0
</code></pre>
<p>That contrast is the whole point: the release moved dev to <code>1.27.2</code>, and Production still runs <code>1.27.0</code> because nothing has promoted it there yet.</p>
<h2>Promote the same release to Production</h2>
<p>Now promote the same release to Production. Because the process has a Production-scoped approval step, the deployment pauses and waits for a human before it touches anything.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/deploy-to-production.png" alt="Promoting the release to Production with an approval step." loading="lazy" }</p>
<p>:::</p>
<p>Production promotion stops at the approval gate; the image update and sync below it are queued, not run.</p>
<p>Approve it, and the same flow runs against the production overlay: Octopus commits the tag to <code>overlays/production</code>, and Argo CD syncs the <code>production</code> namespace. Production now gets exactly what was verified in dev, not a freshly hand-edited value.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/promotion-result.png" alt="Promotion result" loading="lazy" }</p>
<p>::figcaption[Promotion complete. The same release 1.27.2 that ran in Development is now live in Production.]</p>
<p>:::</p>
<p>The project dashboard shows the end state at a glance: one release, both environments, both healthy, with the live status pulled from Argo CD.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/promote-release-with-argo-cd-and-octopus/dashboard.png" alt="Project dashboard" loading="lazy" }</p>
<p>::figcaption[Development at 9:06 PM, Production at 9:43 PM after approval. Same release, one predictable shape.]</p>
<p>:::</p>
<h2>The governance you got for free</h2>
<p>Taking a step back, there are a few things this approach has saved you from:</p>
<ul>
<li>
<p><strong>An immutable release snapshot.</strong> Release <code>1.27.2</code> pinned the exact image version. Production could only ever receive what dev verified.</p>
</li>
<li>
<p><strong>A Git commit per environment.</strong> Each promotion is a commit in your history, attributable and reversible:</p>
</li>
</ul>
<pre><code class="language-bash">07d62d8  Octopus Deploy promoted image 1.27.2   (production overlay)
28e2ace  Octopus Deploy promoted image 1.27.2   (dev overlay)
dfdadc8  Initial GitOps repo
</code></pre>
<ul>
<li>
<p><strong>An approval record.</strong> Production promotion required a named human to take responsibility and proceed, captured in the deployment history.</p>
</li>
<li>
<p><strong>One view of what is running where.</strong> The project dashboard shows every environment and the release it holds, with live health from Argo CD, and you can click into any deployment to see who promoted it and when. That single pane matters more as you scale because your Argo CD Applications might be spread across many instances in different clusters, regions, or accounts, and Octopus gives you one place to see and govern all of them instead of tab-hopping between Argo CD UIs.</p>
</li>
</ul>
<p>None of this is captured by default in the hand-edited-overlay approach. Because the Octopus release is a standard, predictable object, the same governance and policy apply no matter what sits underneath</p>
<p>This ties into the core Platform Hub idea: a single deployment shape and consistent governance across every stack you run.</p>
<h2>Going from overlay edits to audited releases</h2>
<p>Promotion should not be a YAML edit you hope you got right. With Argo CD connected to Octopus, it becomes a release you can govern.</p>
<p>Argo CD keeps doing what it does best: reconciling Git with your cluster, while Octopus adds a release model and an audit trail to that flow.</p>
<p>The bigger idea here is <a href="https://octopus.com/use-case/platform-hub">Platform Hub</a>, which offers you one place to see what is running where, and the same governance and audit across every environment, cluster, and Argo CD instance you run, not just the one in this walkthrough.</p>
<p>If you promote Argo CD deployments by hand today, that is the gap it closes. See how <a href="https://octopus.com/use-case/platform-hub">Platform Hub</a> brings your GitOps deployments under one governed roof, read <a href="https://octopus.com/blog/manage-releases-rollbacks-argo-cd">Manage releases and rollbacks with Argo CD</a> for the release mechanics, and <a href="https://octopus.com/start">start for free</a>!</p>]]></content>
    </entry>
    <entry>
      <title>Inside Platform Engineering with Joep Piscaer</title>
      <link href="https://octopus.com/blog/inside-platform-engineering-joep-piscaer" />
      <id>https://octopus.com/blog/inside-platform-engineering-joep-piscaer</id>
      <published>2026-07-23</published>
      <updated>2026-07-23</updated>
      <summary></summary>
      <author>
        <name>Matthew Allford, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>The CNCF landscape has hundreds of logos, dozens of categories, and no shortage of people telling you what belongs in your platform. Joep Piscaer, Field CTO at Portainer, joined me on Inside Platform Engineering with a take I don't hear often enough: that the best platform decision is frequently to add nothing at all.</p>
<p>Joep calls the CNCF landscape a candy shop, and it's a comparison that stuck with me. Just because something's on the shelf doesn't mean it belongs in your cart, and the cost of a bad choice doesn't show up at checkout, it shows up months or years later when someone has to support it.</p>
<h2>Watch the episode</h2>
<p>You can watch the episode with Joep below.</p>
<p><a href="https://www.youtube.com/watch?v=uMn978s5FkE">Inside Platform Engineering with Joep Piscaer</a></p>
<h2>The candy shop problem</h2>
<p>Joep's argument is simple but easy to forget in practice, which is that every tool you add to your platform is a tool you now have to operate, secure, and explain to whoever inherits it. He's not against new tooling on principle, he's against choosing it reflexively because it's popular or well-marketed. His rule of thumb is that the best choice is often no choice at all, and that resisting the landscape is itself a skill worth developing, not a sign you're falling behind.</p>
<h2>Small teams will build lean platforms</h2>
<p>One of the more provocative points Joep made was that a platform team with a budget and an SLA will naturally start building for its own survival, not just for its users. His suggested fix leans further than most people may be comfortable with, keep the team small, ideally under eight people (this is very contextual to the organization), so there's only time for the basics. I liked how he framed a bloated platform as a freight ship rather than a speedboat. Once it's big, you can only change course by a single degree at a time, no matter how good your intentions are.</p>
<h2>Talk to your users before you build</h2>
<p>Joep was adamant that understanding why you're building something matters more than the build itself, going as far as to say a good developer might spend as little as 20% of their time actually writing code. The rest goes into figuring out what's actually needed. This came up multiple times throughout our conversation. His advice for platform teams is to get out of meetings and sit next to the people doing the work, which he only half-jokingly compared to Fisher-Price's old "soul-crushing meeting" toy.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/inside-platform-engineering-joep-piscaer/soul-crushing-meeting.png" alt="An image of a satirical Fisher-Price toy box parody designed by Daniel Picard." loading="lazy" }</p>
<p>:::</p>
<h2>Vibe coding is changing who the platform needs to support</h2>
<p>We spent time on how AI-assisted coding is reshaping who your platform needs to serve. Joep's read is that business users are increasingly vibe-coding their own tools because commodity software rarely fits the way their teams actually work. Once they've got something that works, they just want a URL, not a ticket in your backlog or a crash course in Kubernetes. What struck me was Joep's parallel back to Platform Engineering itself. Just as we're told to go and understand what our users actually need rather than guessing, these business users are doing exactly the same thing for themselves, they just build it rather than ask for it. The job for a platform then becomes hiding all of that complexity so those tools can be deployed simply, while staying lean, secure, and compliant underneath. Whether that's a threat to platform teams or an opportunity probably depends on how ready your platform already is to support something it didn't design.</p>
<p>Happy deployments!</p>
<p>:::div{.hint}</p>
<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>
<p>:::</p>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.7: Modern multi-tenancy</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e7" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e7</id>
      <published>2026-07-22</published>
      <updated>2026-07-22</updated>
      <summary>Find out why traditional SaaS multi-tenancy has been replaced with a superior approach.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Multi-tenancy is an old concept that dates back to machine sharing. It got a new life with the rise of SaaS and the need to share infrastructure and databases across many tenants. Still, with the rise of lightweight virtualization, the idea of managing multi-tenancy with high code complexity has lost its appeal.</p>
<p>Modern multi-tenancy leans into the ease of allocating dedicated instances to customers, improving isolation, reducing the risk of data leaks, and making it trivial to charge a fair price based on use (instead of subsidizing the noisy neighbors with a volume of under-utilizers).</p>
<h2>Watch the episode</h2>
<p>You can watch the episode below, or read on to find some of the key discussion points.</p>
<p><a href="https://www.youtube.com/watch?v=0whxP3T6U5A">Watch Continuous Delivery Office Hours Ep.7</a></p>
<h2>Moving away from tenanted applications</h2>
<p>There are 3 primary architectural approaches to multi-tenancy.</p>
<ul>
<li>A shared application instance and database: Many customers are using the same running instance, with the application taking care of isolating their use and data.</li>
<li>A shared application instance and a dedicated database: While customers all use the same running instance, their data is stored in a dedicated database, which the application connects to based on the tenant.</li>
<li>Fully isolated infrastructure: Every tenant has a dedicated instance and database running on an allocation of compute.</li>
</ul>
<p>The shared approach comes with many drawbacks. The application's code is more complex and requires more testing to reduce the risk of data being displayed to someone who shouldn't see it. Having a dedicated database limits the complexity and risk to the mechanism that selects the appropriate connection string.</p>
<p>When tenants share application instances, databases, or database servers, one tenant can disrupt service for others, for example, by running a resource-intensive operation. It's difficult to pinpoint where the increased load is coming from, and mechanisms for charging based on use often rely on proxy metrics that don't reflect actual use.</p>
<p>With modern hosting options, like containers, it becomes far easier to achieve high resource use at the infrastructure level, removing the need for applications to be made tenant aware. You can avoid all the complexity by giving each tenant their own application and database.</p>
<p>If you have a customer with high resource needs, their use doesn't slow down other customers or cause an outage. If they need a more powerful instance, they can pay to have one.</p>
<h2>Shifting tenants to deployment-time</h2>
<p>To make modern multi-tenancy work, you need to be able to deploy many more instances and apply the correct configuration when you do so. Modern CD tools take care of this, so you can share a single deployment process with hundreds or thousands of tenants.</p>
<p>When CD tools push a software version out, they can install tenant-specific instances by applying configuration variables. If you have 100 tenants, 3 environments, and 5 settings, the CD tool eliminates the need to manage 1,500 configuration files. They can also help you progressively roll out a new version based on tags so that customers can opt for early access or only the most stable versions.</p>
<p>Multi-tenancy has effectively shifted out of your code and is now managed by your deployment pipeline.</p>
<h2>A tenant isn't always a customer</h2>
<p>We often think of tenants as "customers," but they can just as easily be physical locations, like a hospital, restaurant, or retail store, that need a dedicated instance. Any time you need a dedicated instance, a tenanted deployment can provide it.</p>
<p>If you want to learn more, we also have a <a href="https://octopus.com/whitepapers/modern-view-of-multi-tenancy">white paper on modern multi-tenancy</a> available for download.</p>
<p>Happy deployments!</p>
<p>:::div{.hint}</p>
<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>
<p>:::</p>]]></content>
    </entry>
</feed>