Running network labs has changed over the years, especially for POC (Proof of Concept), personal projects or education purposes. Long gone are the days when you had to buy old equipment off eBay and find a home for it in your cupboard or living room. Although I do miss my trusty IKEA lack rack, the cheapest DIY home rack that transforms your coffee table into a mini rack!
If you don't have the space, time or money for physical kit, you're in luck. Nearly all of our beloved network vendors now offer virtualised routers and switches — in part thanks to the adoption of cloud computing and NFV.
When choosing a virtualised lab you aren't short of options to pick from, here are some open-source ones that I've used in the past, in order of preference:
Containerlab has fast become a fan favourite. It stands out for 3 reasons for me!
- All configuration is managed in code (IaC anybody?)
- It uses containers (yay!)
- It's open source!
What does this mean for Network Engineers? Well, it's a good starting place for those of you who haven't delved into the world of containers yet and it just so happens I have the perfect docker post to introduce you to the basics of using containers. As well as using these mythical containers, Containerlab also lets you easily share your network topologies via a topology definition file, which means you can easily store your lab config using git repositories and share it with all of your buddies - or your colleagues if you have no friends!
Installing Containerlab
The official Containerlab installation guide provides a few options for installation but I've found the most straightforward to be using the Containerlab docker image, that's right, we're making a Containerlab container to run other containers!
This means the lab is more portable and should be good to go on any host you want. Here are the commands to pull and run the Containerlab image as a container. Noting that this is mounting the Docker socket from the host, which is referred to as Docker-outside-of-Docker (DooD):
docker pull ghcr.io/srl-labs/clab
docker run --rm -it --privileged \
--network host \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/run/netns:/var/run/netns \
-v /etc/hosts:/etc/hosts \
-v /var/lib/docker/containers:/var/lib/docker/containers \
--pid="host" \
-v $(pwd):$(pwd) \
-w $(pwd) \
ghcr.io/srl-labs/clab bash
This approach uses the docker pull command to pull a published image, which is already built and pushed to the registry and project location "ghcr.io/srl-labs" under the image name "clab".
Okay, so that explains the docker pull but what about the docker run command? That's a bit of a mouthful and who's going to remember that? Time to use my old friend docker compose — a container definition file that lets you spin up multiple containers and store the unwieldy build options in a single file.
Create a YAML file called docker-compose.yml in the directory you are working in for this project. You can call it anything else if you want but you then need to make sure you specify the full file using the argument -f. Here are our docker pull and docker run arguments put into a single docker compose file:
services:
clab:
image: ghcr.io/srl-labs/clab
container_name: containerlab
stdin_open: true
tty: true
privileged: true
network_mode: host
# Set the working directory to our "present working directory"
working_dir: "$PWD"
# volumes can be mounted from our host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/netns:/var/run/netns
- /etc/hosts:/etc/hosts
- /var/lib/docker/containers:/var/lib/docker/containers
- ${PWD}:$PWD:rw
pid: host
command: bash
Note the ${PWD}:$PWD:rw section, this adds a volume mount from your "present working directory", while
working_diris the home/working directory of the container when it's running or when you exec into it. This is important for running Containerlab commands, as it looks in the current directory for topologies ending inclab.yamlorclab.yml. If it's not in the current working directory you need to specify the topology location with-t <filepath>or--topo <filepath>.
Congratulations! You have a powerful docker-compose file that can pull, build and run the container using a single command:
# From the same directory as the docker compose, -d or --detach starts the container in the background:
docker compose up -d
[+] Running 1/1
⠴ Image ghcr.io/srl-labs/clab [⣿⣿⣿⣿] 53.96MB / 53.96MB Pulling 4.6s
✔ Image ghcr.io/srl-labs/clab Pulled
✔ Container containerlab Started
# If you are specifying the compose file to use:
docker compose -f random-dir/compose-file.yml up -d
Next we can check if the container has come up and view the images we now have available for use:
# view running containers including those which have exited
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
829ab0b61413 ghcr.io/srl-labs/clab "bash" 2 minutes ago Up 2 minutes containerlab
# view the local docker images on you host
docker images
Time to "exec" to the Containerlab container using bash interactively or we can send terminal commands to it directly without starting a terminal session, which is great for sending commands within scripts or CI jobs.
# interactively with a bash shell to the container
$ docker exec -it containerlab /bin/bash
orbstack:/Users/sirhoppington/clab# ls -l
total 16
-rw-r--r-- 1 root root 1812 Jun 22 18:54 interfaces.json
-rw-r--r-- 1 root root 174 Jun 22 18:54 requirements.txt
drwxr-xr-x 1 root root 128 Jun 22 18:54 scripts
-rw-r--r-- 1 root root 1130 Jun 22 18:54 topology.clab.yml
# To send a single command
$ docker exec -it containerlab bash -c "containerlab version"
⣴⡾⠛⠛⠖ ⢠⣶⠟⠛⢷⣦ ⢸⣿⣧ ⣿ ⠘⠛⢻⡟⠛⠛ ⣾⣿⡀ ⣿⡇ ⣿⣿⡄ ⢸⡇ ⣿⡟⠛⠛⠃⢸⣿⠛⠛⣷⡄ ⣿⡇ ⣿⡇
⢸⣿ ⣿⡇ ⣿⡇⢸⣿⠹⣧⡀⣿ ⢸⡇ ⣸⡏⢹⣧ ⣿⡇ ⣿⡏⢿⣄⢸⡇ ⣿⣧⣤⣤ ⢸⣿⣀⣀⣾⠇ ⣿⡇⠐⠟⠛⢿⡆ ⣿⡷⠛⢿⣆
⠘⣿⣄ ⡀ ⢻⣧⡀ ⣠⣿⠃⢸⣿ ⠘⣷⣿ ⢸⡇ ⢠⣿⠷⠶⢿⡆ ⣿⡇ ⣿⡇ ⢻⣾⡇ ⣿⡇ ⢸⣿⠉⢻⣧⡀ ⣿⡇⢰⡟⠛⣻⡇ ⣿⡇ ⣸⡿
⠈⠙⠛⠛⠉ ⠉⠛⠛⠋⠁ ⠘⠛ ⠈⠛ ⠘⠃ ⠚⠃ ⠘⠛ ⠛⠃ ⠛⠃ ⠙⠃ ⠛⠛⠛⠛⠃⠘⠛ ⠙⠓ ⠛⠃⠈⠛⠛⠙⠃ ⠛⠛⠛⠛⠁
version: 0.77.0
commit: 05c2006e3
date: 2026-06-28T08:07:27Z
source: https://github.com/srl-labs/containerlab
rel. notes: https://containerlab.dev/rn/0.77/
We have our containerlab environment set up - good for you! But we want to emulate network devices so we need to get some images for containerlab to use.
Luckily for us, some vendors provide NOS (Network Operating System) images natively. Nokia, who initially developed and open-sourced Containerlab, offers its SR Linux container image for free! Arista gives away its cEOS image too, but you need to register for an account and download it manually. A full list of supported NOSes is detailed in the kinds section of the Containerlab docs - note that some of these are trickier than others to setup if the vendor doesn't supply a native container image.
For this post I will be using an Arista cEOS image and an SR Linux image because they are free. If you aren't able to register for an Arista account, you can just use 2x SR Linux images or take your pick from any of the supported kinds!
Creating our first topology
First get yourself over to the Arista download page (after creating an account) and download your desired image from the cEOS lab section, if you're on an ARM host you should pick an image with version 4.34.0F or later with the format cEOSARM.
The image will be available as a .tar.xz file, once downloaded we will need to import it for use in docker - and therefore also Containerlab. You can name the image what you would like, but it's logical to tag it with the eos version.
# import container image and save it under ceos:4.36.1F name and tag
$ docker import cEOSarm-lab-4.36.1F.tar.xz ceos:4.36.1F
sha256:117a3441879c656cf826dfa6c6421330f4fcc48de0f560187c525c34f48fad19
For Nokia SR Linux we're blessed with a straightforward pull from a public container registry. No sign ups, no portals, lovely stuff!
$ docker pull ghcr.io/nokia/srlinux
Using default tag: latest
latest: Pulling from nokia/srlinux
e5faa7114cc8: Pull complete
Digest: sha256:b0ba6089046663415287a134f1e3086329e1a8d0fcbc969df41c8a52951dd2ca
Status: Downloaded newer image for ghcr.io/nokia/srlinux:latest
ghcr.io/nokia/srlinux:latest
We have our images so it's time to create a basic topology definition file to get us up and running:
# topology.clab.yml
name: ci
topology:
kinds:
arista_ceos:
binds:
- interfaces.json:/mnt/flash/EosIntfMapping.json:ro
env:
CLAB_MGMT_VRF: OOB
links:
- endpoints:
- router-1:eth1
- router-2:e1-1
nodes:
router-1:
image: ceos:4.36.1F
kind: arista_ceos
startup-config: startup-configs/router-1.cfg
router-2:
kind: nokia_srlinux
type: ixr-d3
image: ghcr.io/nokia/srlinux
startup-config: startup-configs/router-2.cfg
Let's break this down. We only really need two parts of this definition file to get our first lab up and running: the nodesand thelinks sections. The former defines our specific nodes and each specific attribute, including the name, the kind and importantly the image we will use for the specific node (and any host specific config, such as startup-configs, entry scripts or type for Nokia images). While the latter shows the mapping of endpoints for each node, which maps to a veth pair.
I have defined a unique startup-config for each node which is not strictly required but enables you to specify a startup config per node, some kinds also permit partial startup-configs which are applied in addition to the Containerlab default config (such as ssh, mgmt, netconf config).
A minimalistic topology file would therefore look like this, which simply connects the two endpoints via eth1 & e1-1 on the nodes respectively:
name: ci
topology:
links:
- endpoints:
- router-1:eth1
- router-2:e1-1
nodes:
router-1:
image: ceos:4.36.1F
kind: arista_ceos
router-2:
kind: nokia_srlinux
type: ixr-d3
image: ghcr.io/nokia/srlinux
Launching our topology is as simple as running Containerlab deploy either locally if you have Containerlab directly on your host or if you are running on our container you will need to execute this via the container itself:
# Get an interactive shell to the containerlab container
$ docker exec -it containerlab bash
orbstack:/Users/sirhoppington/clab# containerlab deploy
14:02:51 INFO Containerlab started version=0.77.0
14:02:51 INFO Parsing & checking topology file=topology.clab.yml
╭──────────────────┬───────────────────────┬─────────┬───────────────────╮
│ Name │ Kind/Image │ State │ IPv4/6 Address │
├──────────────────┼───────────────────────┼─────────┼───────────────────┤
│ clab-ci-router-1 │ arista_ceos │ running │ 172.20.20.2 │
│ │ ceos:4.36.1F │ │ 3fff:172:20:20::2 │
├──────────────────┼───────────────────────┼─────────┼───────────────────┤
│ clab-ci-router-2 │ nokia_srlinux │ running │ 172.20.20.3 │
│ │ ghcr.io/nokia/srlinux │ │ 3fff:172:20:20::3 │
╰──────────────────┴───────────────────────┴─────────┴───────────────────╯
If you are using the Containerlab docker-compose from this post, you can also replace the
commandsection of the file to:containerlab deploy. This executes the Containerlab topology command when you rundocker compose up, however it will exit once the topology is up - so you can't play around with it!
If you have deployed this lab previously and the containers are still there you will get a prompt to add the --reconfigure flag to your deploy command:
# clab deploy
08:33:57 INFO Containerlab started version=0.77.0
08:33:57 INFO Parsing & checking topology file=topology.clab.yml
ERROR
Containers ["clab-ci-router-1" "clab-ci-router-2"] already exist. Add '--reconfigure' flag to the deploy command to
first remove the containers and then deploy the lab.
```
The final output of our `containerlab deploy` shows that our lab was deployed successfully, you can also issue `containerlab inspect` to view the nodes deployed as part of our topology again or to check the containers state (similar to docker ps). We can see from the output that we have a new lab directory created called `clab-ci`, by default Containerlab prepends your lab with "clab"-"<TOPOLOGY-NAME>". This new directory will be used to store artifacts generated by Containerlab specific to the topology and individual nodes:
```bash
orbstack:/Users/sirhoppington/clab# ls -l clab-ci
total 16
-rw-r--r-- 1 root root 1043 Jul 1 14:03 ansible-inventory.yml # an auto-generated ansible inventory
-rw-r--r-- 1 root root 1 Jul 1 14:02 authorized_keys
-rw-r--r-- 1 root root 212 Jul 1 14:03 nornir-simple-inventory.yml # an auto-generated nornir inventory
drwxr-xr-x 1 root root 128 Jul 1 14:02 router-1
drwxr-xr-x 1 root root 192 Jul 1 14:02 router-2
-rw-r--r-- 1 root root 3016 Jul 1 14:03 topology-data.json
If everything has gone to plan we should be able to access our new nodes directly, though how you get to them will vary depending on your installation method. As I am using DooD, I can access the new routers directly from the host.
For cEOS we can access via bash to get the Linux shell for the container:
$ docker exec -it clab-ci-router-1 bash
However it's more likely we will want to access the CLI - you're a network engineer so the CLI is where you get your thrills from, right? We can see our devices have successfully been connected together (Eth1 + e1-1) and back to our docker management network (Ma1):
$ docker exec -it clab-ci-router-1 Cli
router-1>en
router-1#show int des
Interface Status Protocol Description
Et1/1 up up
Ma1 up up
We can also note that the Management1 interface has been configured by default. This is the IP used to communicate with the host via the docker network, which we can see has been created as clab below:
router-1#show run int Ma1
interface Management1
ip address 172.20.20.2/24
ipv6 address 3fff:172:20:20::2/64
router-1#exit
$ docker network list
NETWORK ID NAME DRIVER SCOPE
45a1375f0778 bridge bridge local
9a3b3a871108 clab bridge local
For SSH access if you're running Containerlab as a container, you need to exec into the container to SSH to it by default, because Containerlab updates the hosts file in the container OS (not the host):
docker exec -it containerlab bash
orbstack:/Users/sirhoppington/clab# ssh admin@clab-ci-router-1
Warning: Permanently added 'clab-ci-router-1' (ED25519) to the list of known hosts.
(admin@clab-ci-router-1) Password:
router-1>en
router-1#
Kinds and groups
If we look back at the original topology definition file we provided a couple of extra fields, notably under kinds we have defined values which will be applied to all nodes of that specific kind. This allows us to provide default values we want to be inherited by all of our nodes of that kind. In my example I've configured a bind which maps the interfaces.json from the local host, to /mnt/flash/EosIntfMapping.json for all arista_ceos kinds.
topology:
kinds:
arista_ceos:
binds:
- interfaces.json:/mnt/flash/EosIntfMapping.json:ro
env:
CLAB_MGMT_VRF: OOB
The interfaces.json file can be used for Arista cEOS devices and allows you to map the interface naming conventions, here the interface eth1 which was specified in the topology translates to Ethernet1/1 in the node configuration:
# interfaces.json
{
"ManagementIntf": {
"eth0": "Management1"
},
"EthernetIntf": {
"eth1": "Ethernet1/1",
"eth2": "Ethernet2/1",
"eth3": "Ethernet3/1",
"eth56": "Ethernet56/1"
}
}
Now kinds is helpful for kind-specific defaults, but sometimes you will want to share some values across different kinds -> enter stage left groups. This applies the same logic but across all kinds. Here is an example of how it is applied. We create a new group called core-pes and each node assigned to that group will inherit the env CLAB_MGMT_VRF.
topology:
groups:
core-pes:
env:
CLAB_MGMT_VRF: OOB
nodes:
pe1:
group: core-pes
pe2:
group: core-pes
Note: If you specify a different option on a specific host within the
nodesconfiguration it will override the default fromkindsorgroups- helpful for providing a default image (software version) but testing a new image version on only one of the nodes. Here is the inheritance model:node -> group -> kind -> defaults
Kill it with fire
You now have a simple 2-node topology - but it's probably late and you want to turn off your laptop. No worries, you can just run containerlab destroy to bring the topology down gracefully!
orbstack:/Users/sirhoppington/clab# containerlab destroy
14:17:54 INFO Destroying lab name=ci
14:17:55 INFO Removed container name=clab-ci-router-2
14:17:55 INFO Removed container name=clab-ci-router-1
14:17:55 INFO Removing host entries path=/etc/hosts
14:17:55 INFO Removing SSH config path=/etc/ssh/ssh_config.d/clab-ci.conf
Tip: If typing
containerlabis too much for you, you can useclabinstead:clab <command>.
Fin
Ok, that's a wrap! You now have the basis to build a Containerlab topology from scratch, it's up to you to explore what you can create with it - change the topology, integrate tests etc. There are loads of cool things you can do with Containerlab and this post hardly scratched the surface of what's possible - but hopefully it's enough to get you setup and ready to create some cool labs!
I'll leave you with a bunch of customisable fields that can be added to the topology file, these are some of my most commonly used ones, but it's worth getting familiar with the full range detailed on the Containerlab site: topology definition.
topology:
# Defines the subnets for the mgmt docker network, including the gateway address to use.
mgmt:
ipv4-gw: 192.0.2.254
ipv4-subnet: 192.0.2.0/24
ipv6-gw: 2001:db8::254
ipv6-subnet: 2001:db8::/64
network: custom_mgmt_network
kinds:
arista_ceos:
# Environment variables: set the MGMT VRF
env:
CLAB_MGMT_VRF: MGMT
# Commands run on each node after deploy; here a self-signed cert to enable eAPI via the MGMT VRF
exec:
- sleep 10
- FastCli -p 15 -c 'security pki key generate rsa 2048 eapi-key.key'
- FastCli -p 15 -c 'security pki certificate generate self-signed eapi-self-signed.crt key eapi-key.key validity 9000 parameters common-name router-1'
- FastCli -p 15 -c 'conf t \n management api http-commands \n vrf MGMT \n no shutdown'
# Default image for this kind
image: ceos:latest
links:
- type: veth
endpoints:
- node: <NodeA-Name> # mandatory
interface: <NodeA-Interface-Name> # mandatory
mac: <NodeA-Interface-Mac> # optional
ipv4: <NodeA-IPv4-Address> # optional e.g. 192.0.2.1/24
ipv6: <NodeA-IPv6-Address> # optional e.g. 2001:db8::1/64
- node: <NodeB-Name> # mandatory
interface: <NodeB-Interface-Name> # mandatory
mac: <NodeB-Interface-Mac> # optional
ipv4: <NodeB-IPv4-Address> # optional
ipv6: <NodeB-IPv6-Address> # optional
mtu: <link-mtu> # optional
$ comments