<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Containerlab · Network Doodles</title><link>https://networkdoodles.com/tags/containerlab/</link><description>Network automation, from first playbook to agentic operations. Ansible, NetBox, MCP, Python and the occasional doodle.</description><language>en-us</language><managingEditor>David Henderson</managingEditor><lastBuildDate>Tue, 10 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://networkdoodles.com/tags/containerlab/index.xml" rel="self" type="application/rss+xml"/><item><title>Why I Built CapeOS, an Open-Source Network OS for Labs</title><link>https://networkdoodles.com/posts/capeos-lab-network-os/</link><pubDate>Tue, 10 Nov 2026 00:00:00 +0000</pubDate><dc:creator>David Henderson</dc:creator><guid>https://networkdoodles.com/posts/capeos-lab-network-os/</guid><category>capeos</category><category>containerlab</category><category>network-automation</category><category>ansible</category><category>anta</category><category>testing</category><description>Why I built CapeOS, a free network OS for labs on Linux and FRR: its Bolt and Pulse dialects, what works in 0.11.0, and how to run it in containerlab.</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<div class="details admonition abstract open">
        <div class="details-summary admonition-title "><i class="icon admonition-icon icon-abstract"></i>Short answer<i class="details-icon  admonition-icon admonition-icon-arrow-right"></i></div>
        <div class="details-content">
            <div class="admonition-content">CapeOS is an open-source (Apache 2.0) network operating system for labs. It runs on Linux with FRRouting
underneath, so the routing is real, and it gives each node a familiar command line in one of two dialects:
<strong>Bolt</strong>, an IOS-style CLI, and <strong>Pulse</strong>, an EOS-style CLI with a JSON-RPC command API. I built it because the
Network Doodles labs needed devices anyone can run for free in containerlab or GitHub Codespaces, with a CLI
beginners already recognise. Version 0.11.0 is out today, and the images are public.</div>
        </div>
    </div>
<p>CapeOS is not affiliated with, endorsed by or sponsored by Cisco Systems or Arista Networks. Cisco, IOS, Arista
and EOS are trademarks of their respective owners. I use the names only to describe the style of command line,
and CapeOS isn&rsquo;t a copy of either company&rsquo;s software.</p>
<hr>
<h2 id="why-did-the-labs-need-their-own-network-os">Why did the labs need their own network OS?</h2>
<p>Every lab on this site has the same rule: you click a button, a codespace opens in your browser, and you&rsquo;re
looking at a working network a few minutes later. No downloads, no accounts on a vendor site, no licence keys.
That rule ruled out most of the network operating systems people actually learn on.</p>
<p>I started where a lot of containerlab users start, with Nokia SR Linux. It&rsquo;s free to pull, it boots in a
container, and it&rsquo;s a good NOS. The first build of the Ansible 101 lab ran on it. But most people coming to
these labs learned networking on IOS. They know <code>show running-config</code>, <code>configure terminal</code> and
<code>interface GigabitEthernet1</code>. SR Linux&rsquo;s model-driven CLI is a different way of thinking about a device, and for
a beginner that&rsquo;s one more new thing stacked on top of the thing they came to learn. In an Ansible lab, I want
the hard part to be Ansible.</p>
<p>Then there&rsquo;s EOS. The <span class="link-soon">ANTA course</span> tests switches through EOS&rsquo;s JSON-RPC API, so it needs
something that speaks that API. Arista&rsquo;s lab image, cEOS-lab, needs an arista.com account to download, and its
licence doesn&rsquo;t let anyone redistribute it. A lab can&rsquo;t ship it, and a lab that tells you to go download it
yourself loses most beginners at step one.</p>
<p>So the choice was: teach on a CLI most beginners don&rsquo;t know, ask them to fetch an image they can&rsquo;t get without
an account, or build something. I built something.</p>
<h2 id="what-is-capeos">What is CapeOS?</h2>
<p>CapeOS is a network operating system for labs, classes, CI pipelines and laptops. Each node is one container.
Underneath it&rsquo;s Debian Linux with <a href="https://frrouting.org/">FRRouting</a>, so when CapeOS says it has an OSPF
neighbour, FRR really formed that adjacency and the kernel really forwards the packets. On top sits a command
line, a device model that owns the running config, and a daemon that applies that config to FRR and the kernel.</p>
<p>A few numbers from the <a href="https://gitlab.com/networkdoodles/capeos">README</a>: a node boots in about 1.5 seconds and
uses about 45 MB of memory, and images are built for both linux/amd64 and linux/arm64. That&rsquo;s what makes a
six-switch fabric fit inside a free codespace.</p>
<p>It&rsquo;s for education, not production. It hasn&rsquo;t been security hardened or audited, some commands are accepted and
stored but do nothing, and there&rsquo;s no support or stability promise between releases. Don&rsquo;t put it anywhere that
carries real traffic.</p>
<p>It&rsquo;s also a clean-room build. CapeOS reimplements behaviour from observation and from public client code, never
from another product&rsquo;s code or documentation text.</p>
<h2 id="what-are-the-two-dialects">What are the two dialects?</h2>
<p>One core, two command lines. Each dialect is its own image, and both share the same routing, switching and
services underneath.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>Bolt</strong></th>
          <th><strong>Pulse</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Image</td>
          <td><code>capeos-bolt</code></td>
          <td><code>capeos-pulse</code></td>
      </tr>
      <tr>
          <td>Command style</td>
          <td>IOS-style</td>
          <td>EOS-style</td>
      </tr>
      <tr>
          <td>Data ports</td>
          <td><code>GigabitEthernet1</code>, <code>GigabitEthernet2</code>, &hellip;</td>
          <td><code>Ethernet1</code>, <code>Ethernet2</code>, &hellip;</td>
      </tr>
      <tr>
          <td>Management port</td>
          <td><code>GigabitEthernet0</code></td>
          <td><code>Management0</code></td>
      </tr>
      <tr>
          <td>A fresh node is</td>
          <td>a router (<code>CAPEOS_PLATFORM=switch</code> makes a switch)</td>
          <td>a switch: Layer 2 ports, IP routing off</td>
      </tr>
      <tr>
          <td>Programmatic access</td>
          <td>the CLI over SSH, SNMP</td>
          <td>the CLI over SSH, SNMP, and a JSON-RPC command API over HTTPS with JSON output for the common show views</td>
      </tr>
      <tr>
          <td>Config workflow</td>
          <td>running and startup config, archive, <code>configure replace</code> with rollback</td>
          <td>named configure sessions with diffs, commit, commit timers and abort, configuration replace</td>
      </tr>
      <tr>
          <td>First released</td>
          <td>0.1.0</td>
          <td>0.7.0</td>
      </tr>
  </tbody>
</table>
<p>Bolt is the one beginners meet first. Pulse exists because of the API: it&rsquo;s what lets tools that talk JSON-RPC
to a switch, ANTA among them, run in a lab you open with one click.</p>
<p>The names are mine. Bolt and Pulse are CapeOS dialects, not products of anyone else&rsquo;s.</p>
<h2 id="what-works-in-capeos-0110">What works in CapeOS 0.11.0?</h2>
<p>&ldquo;Real&rdquo; below means a protocol actually runs: FRR, the Linux bridge or nftables does the work, not a canned reply.
Most of it is in both dialects; the full list, with a spec for every feature and which dialect has it, is in the
repository.</p>
<table>
  <thead>
      <tr>
          <th>Area</th>
          <th>What&rsquo;s real today</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Routing (FRR)</td>
          <td>OSPFv2 with multiple processes, OSPFv3, BGP IPv4, static routes, VRFs, IPv6, prefix lists and route maps, BFD, BGP aggregates; EIGRP for IPv4 through a CapeOS speaker of its own (Bolt)</td>
      </tr>
      <tr>
          <td>Switching (Linux bridge)</td>
          <td>VLANs, access and trunk ports, SVIs, port-channels with LACP, per-VLAN spanning tree (classic and rapid) with portfast, BPDU guard, root and loop guard</td>
      </tr>
      <tr>
          <td>Data centre (Pulse)</td>
          <td>VXLAN with BGP EVPN, the anycast gateway, MLAG pairs</td>
      </tr>
      <tr>
          <td>Access lists (nftables)</td>
          <td>standard and extended, with hit counters</td>
      </tr>
      <tr>
          <td>Services</td>
          <td>NAT and a DHCP server and relay (Bolt), router advertisements, SLAAC and DHCPv6, HSRP and VRRP with track objects</td>
      </tr>
      <tr>
          <td>Management</td>
          <td>local users, SSH, AAA with TACACS+, NTP, LLDP, remote syslog, an SNMP v2c/v3 agent with IF-MIB, traps and informs, ping and traceroute</td>
      </tr>
  </tbody>
</table>
<p>What&rsquo;s <strong>recorded only</strong>: some commands, such as EIGRP for IPv6, MST and IPv6 first hop redundancy groups, are
accepted, shown in the running config and round trip through automation, but nothing acts on them. That&rsquo;s on
purpose: a playbook that pushes them shouldn&rsquo;t fail, but I&rsquo;d rather say they do nothing than pretend.</p>
<p>0.11.0 itself added ping and traceroute, <code>clear counters</code>, persistent logging and the views ANTA&rsquo;s checks need to
Pulse, and DHCP host pools, ICMP unreachables for access-list denies and <code>show clock</code> to Bolt.</p>
<h2 id="which-tools-have-been-tested-against-it">Which tools have been tested against it?</h2>
<p>I&rsquo;m only going to list what the labs on this site run and check:</p>
<ul>
<li><strong>Ansible</strong>, with the <code>cisco.ios</code> and <code>ansible.netcommon</code> collections over <code>network_cli</code>, against Bolt switches.
The <span class="link-soon">Ansible 101 course</span> and the observability lab use it to push and read config.</li>
<li><strong>ANTA 1.10.0</strong>, against a six-switch Pulse fabric through the command API. All 14 tasks of the ANTA lab pass
on its solution branch with Pulse 0.11.0.</li>
<li><strong>Everyday tools</strong> in the Networking 101 and Wireshark 101 labs: SSH, ping, traceroute, DHCP, DNS and packet
captures on the links between Bolt routers and switches.</li>
</ul>
<p>It&rsquo;s a lab NOS. It isn&rsquo;t the real thing, and it doesn&rsquo;t claim to be. Where a lab result depends on how CapeOS
behaves rather than on the tool you&rsquo;re learning, the lab guide says so.</p>
<h2 id="how-do-i-run-capeos-in-containerlab">How do I run CapeOS in containerlab?</h2>
<p>Pull an image. Both registries carry the same images, and there&rsquo;s no <code>latest</code> tag, so always pin a version:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker pull ghcr.io/dkhenderson2/capeos-bolt:0.11.0
</span></span><span class="line"><span class="cl"><span class="c1"># or: registry.gitlab.com/networkdoodles/capeos/capeos-bolt:0.11.0</span>
</span></span></code></pre></div><p>A CapeOS node is a containerlab <code>kind: linux</code> node. Here&rsquo;s two Bolt routers and one link:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">capeos-two</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">topology</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">defaults</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">linux</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/dkhenderson2/capeos-bolt:0.11.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CAPEOS_USER</span><span class="p">:</span><span class="w"> </span><span class="l">admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CAPEOS_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">capeos-lab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nodes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">r1</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">r2</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">links</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">endpoints</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;r1:eth1&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;r2:eth1&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Deploy it with <code>sudo containerlab deploy -t capeos-two.clab.yml</code>, then <code>ssh admin@clab-capeos-two-r1</code>. The node
is named after its containerlab node, writes a default startup config on first boot, and reports healthy about
two seconds after it starts. Bind a file to <code>/flash/startup-config</code> to give a node its own config, and
<code>write memory</code> saves back to it. In the CLI, <code>eth1</code> is <code>GigabitEthernet1</code>.</p>
<p>For Pulse, change the image to <code>capeos-pulse:0.11.0</code>. Remember that its Ethernet ports start as switchports and
IP routing starts off, so a routed lab needs <code>ip routing</code> and <code>no switchport</code> in its startup config.</p>
<p>Two gotchas worth knowing up front:</p>
<ul>
<li>On macOS, Docker Desktop doesn&rsquo;t route container addresses to your Mac, so SSH by name won&rsquo;t work there. Use
<code>docker exec -it clab-capeos-two-r1 capeos-cli</code>, or run containerlab in a Linux VM or a codespace.</li>
<li>VRFs need a host kernel with VRF support (<code>sudo modprobe vrf</code> once on Linux). Docker Desktop&rsquo;s kernel doesn&rsquo;t
have it.</li>
</ul>
<p>Everything a lab can rely on (interface names, readiness, environment variables, what each image needs from the
host) is written down in the repository&rsquo;s lab contract, <code>docs/lab-contract.md</code>.</p>
<h2 id="which-network-doodles-labs-run-on-capeos">Which Network Doodles labs run on CapeOS?</h2>
<table>
  <thead>
      <tr>
          <th>Course</th>
          <th>Lab devices</th>
          <th>CapeOS image</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><span class="link-soon">Networking 101</span></td>
          <td>a switch and two routers between Linux hosts</td>
          <td>Bolt 0.11.0</td>
      </tr>
      <tr>
          <td><span class="link-soon">Wireshark 101</span></td>
          <td>the same network, with captures on its links</td>
          <td>Bolt 0.11.0</td>
      </tr>
      <tr>
          <td><span class="link-soon">Ansible 101 for networking</span></td>
          <td>a three-switch leaf-spine fabric</td>
          <td>Bolt</td>
      </tr>
      <tr>
          <td><span class="link-soon">Observability for network engineers</span></td>
          <td>three switches sending syslog and SNMPv3</td>
          <td>Bolt 0.6.0</td>
      </tr>
      <tr>
          <td><span class="link-soon">Testing with ANTA</span></td>
          <td>two spines and four leaves with eBGP</td>
          <td>Pulse 0.11.0</td>
      </tr>
  </tbody>
</table>
<p>As each course goes live, its lab opens in GitHub Codespaces from its lab page, on your own GitHub account&rsquo;s
free allowance. Networking 101 is first: its hands-on half is
<span class="link-soon">Networking 101, part 2</span>.</p>
<h2 id="where-do-i-get-it">Where do I get it?</h2>
<p>The source is at <a href="https://gitlab.com/networkdoodles/capeos">gitlab.com/networkdoodles/capeos</a>, under the Apache
2.0 licence. The images include FRRouting, which is GPL-2.0-or-later and runs as a separate program. Read the
contributing guide and the clean-room rules before you send a change.</p>
<p>I built CapeOS so that anyone can learn network automation on something that looks like the networks they&rsquo;ll
work on, without first needing access to enterprise gear. If you build a lab on it, I&rsquo;d like to see it.</p>
]]></content:encoded></item></channel></rss>