Building a Lightweight Network Lab with Netlab and Containerlab on Ubuntu 24.04

13 min read

On this page

After years of building network labs in EVE-NG and GNS3, I finally decided to give Netlab and Containerlab a proper try.

I first came across Suresh Vinasiththamby’s excellent post, Netlab – The Fastest Way to Build Network Labs, and it was enough to convince me to see what the workflow looked like for myself.

Right now I’m working in Antarctica, and a lot of my study time outside of work has been spent building labs to keep my networking skills sharp. Normally I rely heavily on GNS3, but I had to leave my larger lab server back home, so at the moment I’m working with a much smaller hardware footprint.

That made Netlab especially interesting.

Instead of spending time repeatedly creating routers, wiring interfaces, assigning IP addresses, configuring loopbacks, and building the same routing baseline, Netlab lets us describe the topology we want and automate much of that repetitive setup.

The goal is not to automate away the networking. It is to get to the part of the lab we actually want to study faster.

Suresh’s guide uses Ubuntu 22.04 and is a great reference if that is what you are running. For this walkthrough, I’m using Ubuntu 24.04.04 LTS, so I’ll show the exact steps that worked for me on that release.

I also ran into a couple of issues along the way that were not immediately obvious, so I’ll include those here rather than just showing the happy path.

Lab Environment

Everything in this post is running directly on my laptop:

  • OS: Ubuntu 24.04.04 LTS
  • System: Dell XPS 13 7390
  • CPU: Intel Core i7-1065G7, 8 logical CPUs
  • Memory: 16 GB RAM
  • Storage: 512 GB

It is not a dedicated lab server, which is part of what makes this setup useful for me right now. We will start with a small four-node Arista cEOS topology and see how far a lightweight, container-based workflow can take us.

How the Pieces Fit Together

Before getting into the install, it helps to understand what each part of the stack is doing.

For this setup we are using four main components:

  • Netlab — defines the topology, addressing, device types, and protocol modules
  • Containerlab — creates the actual lab topology and connects the interfaces
  • Docker — runs the network operating system containers
  • Ansible — pushes the generated configuration to the devices

Netlab is essentially the piece tying everything together.

Instead of manually creating four routers, connecting each link, assigning IP addresses, configuring loopbacks, and then enabling OSPF, we can describe what we want in a small YAML file and let Netlab build the baseline for us.

The nice part is that it is not completely hidden behind the automation either. Netlab generates the Containerlab topology, Ansible inventory, host variables, and per-device configuration files, so you can still dig into what it created if you want to understand exactly what is happening.

That was one of the things that sold me on the workflow. I still want to see and understand the networking underneath it, I just do not necessarily want to build the same base configuration by hand every time.

Installing Netlab on Ubuntu 24.04

With the pieces out of the way, we can start building the environment.

The first thing I did was update the local APT package index:

sudo apt update

Then install the Python packages we need:

sudo apt install -y python3-pip python3-venv

On Ubuntu 22.04, you may see guides install Netlab directly with:

pip install networklab

On Ubuntu 24.04, running that against the system Python will likely return:

error: externally-managed-environment

Ubuntu is protecting its system-managed Python environment, so rather than overriding that behavior, I decided to give Netlab its own Python virtual environment.

python3 -m venv --system-site-packages ~/netlab-venv

Activate it:

source ~/netlab-venv/bin/activate

Your shell prompt should now show the active environment:

(netlab-venv) pando@conf-t:~$

From here, install Netlab normally:

pip install networklab

Then verify the installation:

netlab version

On my system this returned:

netlab version 26.08 executable location: /home/pando/netlab-venv/bin/netlab 
package location: ['/home/pando/netlab-venv/lib/python3.12/site-packages/netsim']

The important part is that netlab is now running from inside our virtual environment rather than modifying Ubuntu’s system Python installation.

You can double-check that with:

which python 
which pip 
which netlab

Mine returned:

/home/pando/netlab-venv/bin/python 
/home/pando/netlab-venv/bin/pip 
/home/pando/netlab-venv/bin/netlab

From this point forward, whenever I want to work with Netlab I first activate the environment:

source ~/netlab-venv/bin/activate

That keeps the Netlab tooling separate while still letting Docker, Containerlab, and the rest of the host operate normally.

Installing the Supporting Tools

Netlab itself is only part of the setup. We still need the supporting Ubuntu packages, Ansible, Docker, and Containerlab.

Netlab includes installation helpers for all of these, which makes this part pretty painless.

I ran each installer separately so it was easier to see exactly what was happening and troubleshoot anything that failed.

Install the Ubuntu Prerequisites

With the virtual environment still active:

netlab install ubuntu

Netlab will explain what it is about to install and ask for confirmation:

Are you sure you want to proceed [y/n]:

Enter: y

This installs the Ubuntu packages Netlab expects along with a handful of useful tools and development libraries.

Once complete, you should see:

DONE Completed Mandatory and nice-to have Debian/Ubuntu packages installation

Install Ansible

netlab install ansible

This is one of the reasons we created the Python virtual environment earlier.

Netlab should recognize it automatically:

[INFO] You're running netlab in a Python virtual environment [HINT] Python libraries will be installed in the same environment

Continue with the installation and Netlab will install Ansible along with the Python libraries it uses for device configuration and connectivity.

On my system the final versions were:

ansible: 11.10.0 
ansible-core: 2.18.19 
ansible-pylibssh: 1.4.0

A quick verification is:

netlab version
ansible --version

Netlab should now list the Ansible components instead of showing them as missing.

You can also verify that Python can see Ansible:

python -c "import ansible; print(ansible.__version__)"

Mine returned:

2.18.19

Install Docker and Containerlab

Finally:

netlab install containerlab

This installs Docker and Containerlab along with the packages required to run them.

Again, confirm when prompted:

Are you sure you want to proceed [y/n]: y

When the installation finishes, Netlab provides two useful reminders:

* Log out and back in to start using netlab with containerlab/Docker
* Use 'netlab test clab' command to test your installation

At this point I logged completely out of Ubuntu and back in.

Once you log back in, remember that your Python virtual environment will no longer be active. Re-enable it before continuing:

source ~/netlab-venv/bin/activate

Check Your Docker Permissions

Before doing anything else, check your group membership:

groups

Your user needs permission to access Docker without running every command with sudo.

I actually ran into an issue here. Even after logging out and back in, my account was not showing the expected Docker and Containerlab group memberships.

If that happens, make sure your user belongs to both groups:

sudo usermod -aG docker $USER 
sudo usermod -aG clab_admins $USER

You can then log out and back in again, or refresh the group membership in your current shell:

newgrp docker newgrp 
clab_admins

Run:

groups

again and verify that you see:

docker 
clab_admins

Finally, test Docker access:

docker ps

An empty table is completely fine at this point. It simply means Docker is working and there are no containers running yet.

Importing Arista cEOS

With Netlab, Ansible, Docker, and Containerlab working, the next thing we need is a network operating system image.

For this first lab, I’m using Arista cEOS 4.36.2F.

After downloading the cEOS image, I placed the file on my Desktop:

cEOS-lab-4.36.2F.tar.xz

Docker cannot use that archive directly, so we first need to import it and assign it a local image name and tag.

From the directory containing the image:

cd ~/Desktop

Import it into Docker:

docker import cEOS-lab-4.36.2F.tar.xz ceos:4.36.2

The command will take a little while depending on your system. When mine finished, Docker returned the image SHA.

Now verify that Docker has the image:

docker image

On my system:

IMAGE       ID             DISK USAGE    CONTENT SIZE 
ceos:4.36.2 5534ae42226b   3.55GB        976MB

At this point Docker has our cEOS image, but there still should not be any running containers:

Building Our First Topology

With cEOS imported, we can finally get to the interesting part: defining the lab.

For organization, I keep my actual lab files separate from the Python virtual environment. My Netlab environment lives under ~/netlab-venv, while the labs themselves live under ~/netlab-labs.

Inside the first lab directory, create a file named topology.yml.

For this walkthrough, we’ll use a four-router diamond instead of a straight line:

The topology file is still very small:

provider: clab 

defaults.device: eos 
defaults.devices.eos.clab.image: ceos:4.36.2 

module: [ ospf ] 

nodes: [r1, r2, r3, r4] 

links: - r1-r2 - r2-r3 - r1-r4 - r4-r3

There is a lot happening in just a few lines.

provider: clab tells Netlab to use Containerlab as the provider, while:

defaults.device: eos
defaults.devices.eos.clab.image: ceos:4.36.2

sets Arista EOS as the default device type and points Netlab to the local cEOS image we imported earlier.

The actual topology is described with:

nodes: [r1, r2, r3, r4]

links:
  - r1-r2
  - r2-r3
  - r1-r4
  - r4-r3

And this line:

module: [ ospf ]

tells Netlab to include OSPF in the generated configuration.

At this point we have not manually assigned interface addresses, created loopbacks, or configured OSPF on any router. We have simply described the topology and the protocol we want to run.

Bring the lab up with:

netlab up

Netlab will generate the supporting Containerlab and Ansible files, start the cEOS containers, wait for the devices to become reachable, and then push the generated configurations.

On my test run, all four nodes came up successfully and the deployment finished with:

[SUCCESS] Lab devices configured

From there, we can check the lab with:

netlab status

and connect directly to a router with:

netlab connect r1

On r1, LLDP confirmed the diamond exactly as expected:

Port Neighbor Device ID Neighbor Port ID TTL
---- ------------------ ---------------- ---
Et1  r2                 Ethernet1        120
Et2  r4                 Ethernet1        120

And the routing table already showed two equal-cost OSPF paths toward r3‘s loopback:

O      10.0.0.3/32 [110/30] 
          via 10.1.0.2, Ethernet1 
          via 10.1.0.10, Ethernet2

That is exactly what we wanted from the diamond: two valid paths through the topology without manually building any of the underlying configuration.

Before we use this as the baseline for future labs, though, I want to clean up the addressing so management, loopbacks, and point-to-point links are easier to recognize at a glance.

Cleaning Up the Addressing

The default addressing works fine, but I wanted something a little easier to recognize when looking through routing tables, packet captures, and configs.

For this lab, I’m going to separate the address space by purpose:

10.250.0.0/24   Management
10.255.0.0/24   Loopbacks
10.20.0.0/16    Point-to-point links

For the routed links, I’m also using /31s instead of /30s. Since these are true point-to-point links, there is no need to burn four addresses per subnet.

We can define all of this directly in topology.yml:

provider: clab

defaults.device: eos
defaults.devices.eos.clab.image: ceos:4.36.2

addressing:
  mgmt:
    ipv4: 10.250.0.0/24

  loopback:
    ipv4: 10.255.0.0/24

  p2p:
    ipv4: 10.20.0.0/16
    prefix: 31

module: [ ospf ]

nodes: [r1, r2, r3, r4]

links:
  - r1-r2
  - r2-r3
  - r1-r4
  - r4-r3

Rebuild the lab:

netlab down
netlab up

Then verify:

netlab status

Output:

And if you want to confirm the interface addressing directly:

netlab connect r1
r1#show lldp neighbors 
Port Neighbor Device ID Neighbor Port ID TTL
---- ------------------ ---------------- ---
Et1  r2                 Ethernet1        120
Et2  r4                 Ethernet1        120

r1#show ip interface brief 
                                                                                Address
Interface         IP Address            Status       Protocol            MTU    Owner  
----------------- --------------------- ------------ -------------- ----------- -------
Ethernet1         10.20.0.0/31          up           up                 1500           
Ethernet2         10.20.0.4/31          up           up                 1500           
Loopback0         10.255.0.1/32         up           up                65535           
Management0       10.250.0.101/24       up           up                 1500           

r1#show ip route ospf

VRF: default

 O        10.20.0.2/31 [110/20]
           via 10.20.0.1, Ethernet1
 O        10.20.0.6/31 [110/20]
           via 10.20.0.5, Ethernet2
 O        10.255.0.2/32 [110/20]
           via 10.20.0.1, Ethernet1
 O        10.255.0.3/32 [110/30]
           via 10.20.0.1, Ethernet1
           via 10.20.0.5, Ethernet2
 O        10.255.0.4/32 [110/20]
           via 10.20.0.5, Ethernet2

Wrapping Up

At this point, the core setup is complete:

  • Netlab is running inside a dedicated Python virtual environment
  • Ansible is available to Netlab without the earlier import warning
  • Docker and Containerlab are working
  • cEOS is imported locally
  • The four-router diamond topology builds successfully
  • Netlab is generating addressing and OSPF configuration automatically

A few commands are worth keeping handy:

source ~/netlab-venv/bin/activate
netlab up
netlab status
netlab connect r1
netlab down

And if you want to see all active lab instances:

netlab status --all

For me, the biggest takeaway was how quickly Netlab gets you from a small YAML file to a usable topology. I’m still early in using it, but it already feels like a good fit for the kind of repeatable routing labs I want to build without spending time recreating the same base configuration every time.

This first lab was intentionally simple. From here, I’ll be using the same setup for more involved BGP, OSPF, and eventually multi-vendor labs.

Share this: