Learning a routing protocol, reproducing a bug, or rehearsing a configuration change all need the same thing: a network you can break without consequences. The usual options are borrowed hardware, which is scarce and slow to rewire, or a hand-built lab of virtual machines, which takes an afternoon to set up and that nobody else can easily recreate.
Containerlab takes a different approach: the lab is a text file. It lists the nodes and the links between them, and containerlab turns that into a running network in seconds, using container images of real routing software. Because a lab is just files, you can keep it in git, hand it to a colleague, rebuild it identically next month, and delete it without leaving anything behind. The same labs can run in a CI pipeline as network tests.
Nodes can be open source routers, as in this post, or container images of vendor network operating systems such as Nokia SR Linux and Arista cEOS, and containerlab can also run full virtual machines for software that doesn’t come as a container.
This post walks through the whole workflow with a small routed network of three FRR routers: write the topology, deploy it, inspect it, test it, and tear it down. FRR is open source, so you don’t need an account or a license to follow along.
The workflow
You describe the lab in a YAML topology file, and then four commands cover almost everything you do with it:
| Command | What it does |
|---|---|
deploy | Creates the nodes and links described in the file |
inspect | Lists the running nodes and their addresses |
exec | Runs a command on one or all nodes |
destroy | Removes the whole lab |
What you need
- A Linux host, bare metal or a VM
- podman
- containerlab, installed following the official guide
The --runtime podman flag in the commands below tells containerlab to use podman.
The lab
Three routers in a line, running FRR, with static routes between them.
r1 ───────────── r2 ───────────── r3
lo 10.255.0.1/32 lo 10.255.0.3/32
10.0.12.0/24 10.0.23.0/24
| Node | Interface | IPv4 address | Connected to |
|---|---|---|---|
| r1 | eth1 | 10.0.12.1/24 | r2 |
| r1 | lo | 10.255.0.1/32 | — |
| r2 | eth1 | 10.0.12.2/24 | r1 |
| r2 | eth2 | 10.0.23.1/24 | r3 |
| r3 | eth1 | 10.0.23.2/24 | r2 |
| r3 | lo | 10.255.0.3/32 | — |
Get the files from the lab repository and cd into containerlab-basics.
The topology file
name: hello
topology:
kinds:
linux:
image: quay.io/frrouting/frr:10.7.1
nodes:
r1:
kind: linux
binds:
- configs/r1/frr.conf:/etc/frr/frr.conf
- configs/vtysh.conf:/etc/frr/vtysh.conf
r2:
kind: linux
binds:
- configs/r2/frr.conf:/etc/frr/frr.conf
- configs/vtysh.conf:/etc/frr/vtysh.conf
r3:
kind: linux
binds:
- configs/r3/frr.conf:/etc/frr/frr.conf
- configs/vtysh.conf:/etc/frr/vtysh.conf
links:
- endpoints: ["r1:eth1", "r2:eth1"]
- endpoints: ["r2:eth2", "r3:eth1"]
nameis part of every node’s name:clab-<name>-<node>, soclab-hello-r1.kindssets defaults for all nodes of a kind, so the FRR image is written once.nodeslists the routers.bindsmounts a config file from your lab directory into the node, with paths relative to the topology file.linksconnectnode:interfacepairs, and containerlab creates the interfaces with the names you give.eth0is reserved for the management network, so data links start ateth1.
Each router’s frr.conf holds its addresses and static routes. This is r1:
frr defaults traditional
hostname r1
!
interface eth1
ip address 10.0.12.1/24
exit
!
interface lo
ip address 10.255.0.1/32
exit
!
ip route 10.255.0.3/32 10.0.12.2
ip route 10.0.23.0/24 10.0.12.2
vtysh.conf is a single line, service integrated-vtysh-config, which makes FRR’s vtysh shell read this one file.
Deploy
sudo containerlab --runtime podman deploy -t topology.clab.yml
The output ends with a table of the nodes:
╭───────────────┬──────────────────────────────┬─────────┬───────────────────╮
│ Name │ Kind/Image │ State │ IPv4/6 Address │
├───────────────┼──────────────────────────────┼─────────┼───────────────────┤
│ clab-hello-r1 │ linux │ running │ 172.20.20.3 │
│ │ quay.io/frrouting/frr:10.7.1 │ │ 3fff:172:20:20::3 │
├───────────────┼──────────────────────────────┼─────────┼───────────────────┤
│ clab-hello-r2 │ linux │ running │ 172.20.20.4 │
│ │ quay.io/frrouting/frr:10.7.1 │ │ 3fff:172:20:20::4 │
├───────────────┼──────────────────────────────┼─────────┼───────────────────┤
│ clab-hello-r3 │ linux │ running │ 172.20.20.2 │
│ │ quay.io/frrouting/frr:10.7.1 │ │ 3fff:172:20:20::2 │
╰───────────────┴──────────────────────────────┴─────────┴───────────────────╯
Those addresses are on eth0, the management network containerlab creates so you can reach the nodes. Print the table again at any time with containerlab inspect -t topology.clab.yml.
Look around
Run a command inside a node with the container runtime:
sudo podman exec clab-hello-r1 vtysh -c "show ip route static"
IPv4 unicast VRF default:
S>* 10.0.23.0/24 [1/0] via 10.0.12.2, eth1, weight 1, 00:00:02
S>* 10.255.0.3/32 [1/0] via 10.0.12.2, eth1, weight 1, 00:00:02
For an interactive FRR shell, run sudo podman exec -it clab-hello-r1 vtysh.
Containerlab can also run one command on every node:
sudo containerlab --runtime podman exec -t topology.clab.yml \
--cmd "vtysh -c 'show ip route static'"
Test it
r1 pings r3’s loopback, and the packets have to cross r2:
sudo podman exec clab-hello-r1 ping -c 3 10.255.0.3
PING 10.255.0.3 (10.255.0.3): 56 data bytes
64 bytes from 10.255.0.3: seq=0 ttl=63 time=0.159 ms
64 bytes from 10.255.0.3: seq=1 ttl=63 time=0.057 ms
64 bytes from 10.255.0.3: seq=2 ttl=63 time=0.072 ms
--- 10.255.0.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.057/0.096/0.159 ms
The ttl=63 shows the packet crossed one router.
Tear it down
sudo containerlab --runtime podman destroy -t topology.clab.yml --cleanup
--cleanup also removes the clab-hello/ directory containerlab creates next to your topology file. Redeploy whenever you want a fresh start.