Containerlab
Overview
Containerlab is an open-source network emulation orchestrator. You describe a topology in a short YAML file, and Containerlab creates the containers, wires them together with real veth links and applies per-node startup configuration. You get a lab that you can log into, break, fix and destroy in seconds. It is the standard way to test routing designs, automation code and network CI pipelines without touching hardware.
This guide covers the SecureAnyCloud Containerlab image on Ubuntu 24.04 on Azure. The image installs Containerlab 0.77.0 on Docker Engine 29.6.2, bakes in two open-source container images pinned by digest, and ships a working example topology. The image proves itself the moment it boots, and you do not need to source images before you can run anything.
Containerlab is a command-line tool. There is no web interface and no product port, so everything in this guide is done over SSH.
Containerlab is actively developed. Version 0.77.0 was released on 28 June 2026, and the upstream repository was last pushed on 17 July 2026. The repository is not archived and has roughly 2.7k stars. The 0.77.0 changelog includes new features, such as a cisco_xrd_vrouter node kind, SSH IdentityFile support via node credentials and a graph generation fix. This image ships that release unmodified.
Secure by default, with no shared credentials. FRRouting's stock configuration carries the well-known default vty password zebra. This image never ships that password. On the first boot of every VM, containerlab-firstboot.service creates a unique FRRouting vty password for that VM only and writes it to a root-only file.
The Docker daemon is bound to its UNIX socket only, with no TCP listener anywhere. No user is placed in the docker or clab_admins groups, because both memberships are root-equivalent. Every command in this guide uses sudo, which is upstream's documented invocation.
What you get:
- Containerlab 0.77.0 (BSD-3-Clause, Copyright (c) 2021 Nokia), installed system-wide and invoked as
sudo containerlab - Docker Engine 29.6.2 (Apache-2.0) from the official Docker repository, with
docker.serviceandcontainerd.serviceenabled and the daemon on its UNIX socket only - Two baked, digest-pinned, open-source container images,
quay.io/frrouting/frr:10.7.0(GPL-2.0-or-later, 296MB) andalpine:3.24.1(permissive open source, 13MB), so labs deploy with no registry access at all - A bundled four-node example lab,
/opt/sac/labs/sac-frr-lab.clab.yml, wiringhost1 -- router1 ==OSPF== router2 -- host2across a routed core - A bundled two-node minimal lab,
/opt/sac/labs/sac-smoke.clab.yml, for a fast sanity check - A per-VM FRRouting vty password, generated on first boot and written to
/root/containerlab-credentials.txt(0600 root:root)
Prerequisites
You need an active Azure subscription, an SSH public key, a VNet with a subnet in the target region, and a subscription to the Containerlab listing on Azure Marketplace.
Create a network security group (NSG) rule that allows TCP 22 from your administration network. That is the only port this image needs.
Recommended virtual machine size: Standard_D4s_v5 (4 vCPU, 16 GiB RAM). This is a deliberate recommendation, not a default. Containerlab topologies are memory-hungry. A single vendor network operating system node routinely wants 2 to 4 GiB on its own, and multi-node fabrics scale close to linearly, so a nine-node fabric of vendor nodes can want well over 20 GiB.
The bundled example lab is small (four lightweight containers, roughly 400 MiB), so it runs comfortably even on a 4 GiB VM. Size the VM for the topology you intend to build, not for the example.
Deploy from the Azure Portal
In the Azure Marketplace, search for Containerlab, select the SecureAnyCloud publisher and choose Create.
Configure the network security group to allow TCP 22 from your administration network only. There is no product port to open, so no other inbound rule is required.
Deploy from the Azure CLI
Set your variables, create the resource group and create the VM from the gallery image. Replace the values in angle brackets with your own:
RG="netlab-prod"; LOCATION="eastus"; VM_NAME="clab1" GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-sac/providers/Microsoft.Compute/galleries/sacGallery/images/containerlab-ubuntu-24-04/versions/<version>" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$GALLERY_IMAGE_ID" \
--size Standard_D4s_v5 \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku StandardConnect over SSH and read the per-VM credentials
Connect as azureuser, the administrator user you named at deployment time:
ssh azureuser@<vm-public-ip>On first boot, the image generated a FRRouting vty password unique to this VM. It recorded the password, along with the lab paths, in /root/containerlab-credentials.txt. Read the file:
sudo cat /root/containerlab-credentials.txtThe file is 0600 root:root, so only root can read it. The value you need is CLAB_FRR_VTY_PASSWORD. The FRRouting nodes in the bundled lab ask for this password when you enter their CLI. Wherever this guide shows <CLAB_FRR_VTY_PASSWORD>, substitute the value from this file.
FRR's well-known default password zebra is never present on this image. Both first boot and validation fail closed if it ever appears.
Confirm Containerlab, Docker and the baked images
Check the tool versions and confirm which container images are already local. None of these commands needs internet access.
sudo containerlab versionversion: 0.77.0 commit: 05c2006e3 date: 2026-06-28T08:07:27Zsudo docker --version sudo docker info --format 'Server Version: {{.ServerVersion}} Storage: {{.Driver}}'Docker version 29.6.2, build dfc4efb Server Version: 29.6.2 Storage: overlayfssudo docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'REPOSITORY TAG SIZE quay.io/frrouting/frr 10.7.0 296MB alpine 3.24.1 13MBThose two images are the complete set baked into this appliance. Both are pinned by digest, so the version you get is the version whose licence was checked. No vendor network operating system image is included, for the reasons set out in Step 11.
Inspect the bundled topology
A Containerlab lab is one YAML file, and the file is the whole design. Read the bundled example before you deploy it:
sudo head -n 30 /opt/sac/labs/sac-frr-lab.clab.yml# sac example network lab - Containerlab on Ubuntu 24.04 LTS.
#
# host1 ---- eth2:router1:eth1 ==OSPF== eth1:router2:eth2 ---- host2
# 10.0.1.10 10.0.0.0/30 core 10.0.2.10
#
# Every image referenced here is genuinely open source and is ALREADY BAKED INTO THIS IMAGE:
# quay.io/frrouting/frr:10.7.0 GPL-2.0-or-later
# alpine:3.24.1 permissive OSS
#
# To lab a VENDOR network OS (Nokia SR Linux, Arista cEOS, Cisco, Juniper, ...) you must supply that
# image yourself and accept the vendor's own licence terms - see the sac user guide. name: sac-frr-lab topology: nodes: router1: kind: linux image: quay.io/frrouting/frr:10.7.0 binds: - /opt/sac/labs/frr/daemons:/etc/frr/daemons - /opt/sac/labs/frr/router1.conf:/etc/frr/frr.conf router2: kind: linux image: quay.io/frrouting/frr:10.7.0 binds: - /opt/sac/labs/frr/daemons:/etc/frr/daemons - /opt/sac/labs/frr/router2.conf:/etc/frr/frr.conf host1: kind: linux image: alpine:3.24.1The addressing plan is:
Segment, network and addresses:
Routed core (router1 to router2) 10.0.0.0/30 router1 10.0.0.1, router2 10.0.0.2
host1 LAN 10.0.1.0/24 host1 10.0.1.10, router1 10.0.1.1
host2 LAN 10.0.2.0/24 host2 10.0.2.10, router2 10.0.2.1
Loopbacks 10.255.255.0/24 router1 10.255.255.1, router2 10.255.255.2
The two routers run OSPF across the core link, so host1 and host2 are on different subnets with no static path between them. If they can reach each other, routing has converged. Steps 8 and 9 prove this.
The FRRouting node configs live in /opt/sac/labs/frr/router1.conf and router2.conf. They are 0600 root:root because they hold this VM's vty password. The container reads them through the bind mount as root, so the tighter mode costs nothing.
Deploy the lab
Deploy the bundled topology. Containerlab creates the four containers and the veth links, then prints a table of the nodes:
sudo containerlab deploy -t /opt/sac/labs/sac-frr-lab.clab.yml06:41:57 INFO Creating container name=host2 06:41:57 INFO Creating container name=router1 06:41:58 INFO Creating container name=router2 06:41:58 INFO Creating container name=host1 06:41:59 INFO Created link: router1:eth1 ▪┄┄▪ router2:eth1 06:41:59 INFO Created link: host2:eth1 ▪┄┄▪ router2:eth2 06:41:59 INFO Created link: host1:eth1 ▪┄┄▪ router1:eth2 06:41:59 INFO Executed command node=host1 command="ip addr add 10.0.1.10/24 dev eth1" stdout="" 06:41:59 INFO Executed command node=host1 command="ip link set eth1 up" stdout="" 06:41:59 INFO Executed command node=host1 command="ip route add 10.0.2.0/24 via 10.0.1.1" stdout="" 06:41:59 INFO Executed command node=host2 command="ip addr add 10.0.2.10/24 dev eth1" stdout="" 06:41:59 INFO Executed command node=host2 command="ip link set eth1 up" stdout="" 06:41:59 INFO Executed command node=host2 command="ip route add 10.0.1.0/24 via 10.0.2.1" stdout="" 06:41:59 INFO Adding host entries path=/etc/hosts 06:41:59 INFO Adding SSH config for nodes path=/etc/ssh/ssh_config.d/clab-sac-frr-lab.conf ╭───────────────────────────────┬──────────────────────────────┬─────────┬───────────────────╮ │ Name │ Kind/Image │ State │ IPv4/6 Address │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-host1 │ linux │ running │ 172.20.20.5 │ │ │ alpine:3.24.1 │ │ 3fff:172:20:20::5 │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-host2 │ linux │ running │ 172.20.20.3 │ │ │ alpine:3.24.1 │ │ 3fff:172:20:20::3 │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-router1 │ linux │ running │ 172.20.20.2 │ │ │ quay.io/frrouting/frr:10.7.0 │ │ 3fff:172:20:20::2 │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-router2 │ linux │ running │ 172.20.20.4 │ │ │ quay.io/frrouting/frr:10.7.0 │ │ 3fff:172:20:20::4 │ ╰───────────────────────────────┴──────────────────────────────┴─────────┴───────────────────╯Node names are always clab-<lab-name>-<node-name>, so the four nodes are clab-sac-frr-lab-router1, -router2, -host1 and -host2.
The 172.20.20.0/24 addresses are the management network, a Docker bridge private to this VM. The lab's own data-plane addressing is the 10.0.x.x plan from Step 5.
Inspect the running lab
The inspect command re-reads the topology and reports the live state of every node. Run it any time you want to know what is running:
sudo containerlab inspect -t /opt/sac/labs/sac-frr-lab.clab.yml06:42:07 INFO Parsing & checking topology file=sac-frr-lab.clab.yml ╭───────────────────────────────┬──────────────────────────────┬─────────┬───────────────────╮ │ Name │ Kind/Image │ State │ IPv4/6 Address │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-host1 │ linux │ running │ 172.20.20.5 │ │ │ alpine:3.24.1 │ │ 3fff:172:20:20::5 │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-host2 │ linux │ running │ 172.20.20.3 │ │ │ alpine:3.24.1 │ │ 3fff:172:20:20::3 │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-router1 │ linux │ running │ 172.20.20.2 │ │ │ quay.io/frrouting/frr:10.7.0 │ │ 3fff:172:20:20::2 │ ├───────────────────────────────┼──────────────────────────────┼─────────┼───────────────────┤ │ clab-sac-frr-lab-router2 │ linux │ running │ 172.20.20.4 │ │ │ quay.io/frrouting/frr:10.7.0 │ │ 3fff:172:20:20::4 │ ╰───────────────────────────────┴──────────────────────────────┴─────────┴───────────────────╯Log into a router and verify OSPF
Each FRRouting node has a full network operating system CLI, vtysh, which feels familiar if you have used a traditional router shell. Open it interactively on router1:
sudo docker exec -it clab-sac-frr-lab-router1 vtyshThis command is interactive, so run it yourself in a terminal rather than from a script. It needs a real TTY and refuses to start without one.
vtysh prompts for this VM's vty password. Enter the value of CLAB_FRR_VTY_PASSWORD from /root/containerlab-credentials.txt. From the router1# prompt, you can run show ip ospf neighbor, show ip route ospf, show interface brief, configure terminal and the rest of the FRRouting command set.
You can run the same checks non-interactively, as the blocks below show. vtysh -u runs a command in unprivileged mode. Confirm the routing daemon first:
sudo docker exec clab-sac-frr-lab-router1 vtysh -u -c 'show version' </dev/null | sed -n 1pFRRouting 10.7.0_git (router1) on Linux(6.17.0-1010-azure).Now check the OSPF adjacency across the core link:
sudo docker exec clab-sac-frr-lab-router1 vtysh -c 'show ip ospf neighbor' </dev/nullNeighbor ID Pri State Up Time Dead Time Address Interface RXmtL RqstL DBsmL 10.255.255.2 1 Full/- 9.083s 3.923s 10.0.0.2 eth1:10.0.0.1 0 0 0Router2, identified by its loopback 10.255.255.2, is in state Full over eth1 on the core link 10.0.0.1 to 10.0.0.2. Full is the final OSPF adjacency state. It means both routers have exchanged and synchronised their complete link-state databases.
Next, confirm that router1 has learned router2's networks by OSPF, rather than having them configured:
sudo docker exec clab-sac-frr-lab-router1 vtysh -c 'show ip route ospf' </dev/nullCodes: K - kernel route, C - connected, L - local, S - static, R - RIP, O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP, T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR, f - OpenFabric, t - Table-Direct, > - selected route, * - FIB route, q - queued, r - rejected, b - backup t - trapped, o - offload failure IPv4 unicast VRF default: O>* 10.0.2.0/24 [110/20] via 10.0.0.2, eth1, weight 1, 00:00:03 O>* 10.255.255.2/32 [110/10] via 10.0.0.2, eth1, weight 1, 00:00:03The O code marks both routes as OSPF-learned, > marks them as selected and * marks them as installed in the forwarding table. Router1 has learned host2's LAN, 10.0.2.0/24, and router2's loopback. Both are reached via the core neighbour 10.0.0.2, with the standard OSPF administrative distance of 110.
Confirm the interface addressing that underpins this from the node itself:
sudo docker exec clab-sac-frr-lab-router1 ip -br addrlo UNKNOWN 127.0.0.1/8 10.255.255.1/32 ::1/128 eth0@if27 UP 172.20.20.2/24 3fff:172:20:20::2/64 fe80::8019:8aff:fefa:33e6/64 eth1@if29 UP 10.0.0.1/30 fe80::a8c1:abff:fe0a:d86d/64 eth2@if31 UP 10.0.1.1/24 fe80::a8c1:abff:feb6:2419/64eth0 is the management bridge, eth1 is the routed core to router2, eth2 is host1's LAN gateway, and the loopback carries the OSPF router ID 10.255.255.1.
Prove end-to-end connectivity
This is the decisive test. host1 (10.0.1.10) and host2 (10.0.2.10) are on different subnets, two router hops apart, with no direct link. A ping succeeds only if Containerlab wired the veth links correctly and OSPF converged across the core.
First, confirm host1's view of the network:
sudo docker exec clab-sac-frr-lab-host1 ip routedefault via 172.20.20.1 dev eth0 10.0.1.0/24 dev eth1 scope link src 10.0.1.10 10.0.2.0/24 via 10.0.1.1 dev eth1 172.20.20.0/24 dev eth0 scope link src 172.20.20.5Host1 reaches host2's subnet only through its router, 10.0.1.1. Now ping across the routed core:
sudo docker exec clab-sac-frr-lab-host1 ping -c 4 10.0.2.10PING 10.0.2.10 (10.0.2.10): 56 data bytes 64 bytes from 10.0.2.10: seq=0 ttl=62 time=0.106 ms 64 bytes from 10.0.2.10: seq=1 ttl=62 time=0.091 ms 64 bytes from 10.0.2.10: seq=2 ttl=62 time=0.090 ms 64 bytes from 10.0.2.10: seq=3 ttl=62 time=0.090 ms --- 10.0.2.10 ping statistics --- 4 packets transmitted, 4 packets received, 0% packet loss round-trip min/avg/max = 0.090/0.094/0.106 msAll four packets arrive, with zero loss. Note the ttl=62. Packets left host1 with a TTL of 64 and each router decremented it once, twice in total. This is direct evidence that traffic was routed through router1 and router2 rather than bridged.
Destroy the lab
Labs are disposable. Destroy the lab and the VM returns to a clean state:
sudo containerlab destroy -t /opt/sac/labs/sac-frr-lab.clab.yml --cleanup06:42:40 INFO Parsing & checking topology file=sac-frr-lab.clab.yml 06:42:40 INFO Parsing & checking topology file=sac-frr-lab.clab.yml 06:42:40 INFO Destroying lab name=sac-frr-lab 06:42:40 INFO Removed container name=clab-sac-frr-lab-host2 06:42:41 INFO Removed container name=clab-sac-frr-lab-router2 06:42:41 INFO Removed container name=clab-sac-frr-lab-host1 06:42:41 INFO Removed container name=clab-sac-frr-lab-router1 06:42:41 INFO Removing host entries path=/etc/hosts 06:42:41 INFO Removing SSH config path=/etc/ssh/ssh_config.d/clab-sac-frr-lab.confConfirm nothing survived:
sudo docker ps -aq --filter name=clab-sac-frr-lab- | wc -l0Containerlab also removed the /etc/hosts entries and SSH configuration it added, so re-deploying the lab starts from a clean slate.
A second, minimal lab is bundled at /opt/sac/labs/sac-smoke.clab.yml. It has two Alpine nodes at 10.99.0.1 and 10.99.0.2 across a single link. Use it for the fastest possible check that Docker and Containerlab are healthy.
Finally, confirm that the VM's network surface is unchanged after the lab activity:
sudo ss -tlnpState Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1022,fd=3),("systemd",pid=1,fd=129)) LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=550,fd=17)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=550,fd=15)) LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=1022,fd=4),("systemd",pid=1,fd=132))sshd on port 22 is the only listener reachable from the network. The two systemd-resolved entries on 127.0.0.54 and 127.0.0.53 are loopback-only local DNS and are not reachable from the network. There is no Docker API on 2375 or 2376, because the daemon is on its UNIX socket only.
Add your own vendor network OS images
Containerlab can orchestrate vendor network operating systems. Version 0.77.0 supports more than 60 node kinds, including Nokia SR Linux, Arista cEOS, Cisco (c8000, XRd, IOL), Juniper (cRPD, vJunos), SONiC, Cumulus VX and many more.
This image deliberately bundles none of them. Those images are proprietary or require you to obtain them from the vendor and accept the vendor's own licence terms. SecureAnyCloud cannot accept those terms on your behalf and cannot redistribute those images, so this appliance ships only the two open-source images from Step 4. The bundled example lab therefore uses the generic linux node kind.
To lab a vendor NOS, supply the image yourself:
1. Obtain the image from the vendor, under your own account and licence. Some vendors publish to a public registry, some require a portal download and some require a support contract.
2. Make it available to Docker on this VM. For a registry image, authenticate to that registry and pull it. For a downloaded tarball, copy it to the VM and load it:
sudo docker load -i /path/to/vendor-image.tar
sudo docker images3. Reference it in your topology with the correct node kind. Copy the bundled lab as a starting point and edit the kind and image fields for your nodes. Consult the Containerlab documentation for the exact kind string and any per-kind requirements of your vendor's image:
mkdir -p ~/labs
sudo cp /opt/sac/labs/sac-frr-lab.clab.yml ~/labs/my-lab.clab.yml
sudo chown "$USER":"$USER" ~/labs/my-lab.clab.yml4. Size the VM for it. This is where Standard_D4s_v5 matters. A single vendor NOS node commonly reserves 2 to 4 GiB of RAM and one or more dedicated vCPUs, and the requirements add up across the topology. If nodes fail to reach running, check memory first.
Licensing note: importing a vendor image binds you to that vendor's licence terms for that image. SecureAnyCloud supplies the lab host and the two open-source images described above, and nothing more.
Security recommendations
Keep SSH as the only open port. This appliance has no product port and no web interface. Allow TCP 22 from your administration network only, and do not add inbound NSG rules for anything else.
Never expose the Docker socket or the Docker API over TCP. The daemon on this image is bound to its UNIX socket only, and there is deliberately no listener on 2375 or 2376. An exposed Docker API is a full remote root compromise of the VM, with no authentication in the default configuration. Do not enable a TCP socket, and do not forward /var/run/docker.sock into anything untrusted.
Lab nodes are host-local by design. Nodes sit on the Docker bridge 172.20.20.0/24, which exists only on this VM. They are reachable from the host and from each other, but they are not routable from your VNet or the internet. Reach them with sudo docker exec over your SSH session rather than by publishing ports.
Membership of docker and clab_admins is root-equivalent. No user on this image is in either group, and that is intentional. Every command in this guide uses sudo, which is upstream's documented invocation. Adding a user to docker silently grants that user full root on the VM.
Protect the per-VM credential. The FRRouting vty password lives in /root/containerlab-credentials.txt (0600 root:root). The FRR node configs that carry it are also 0600 root:root. The well-known default password zebra is never shipped. If you rotate the password, update the node configs and the credentials file together.
Treat lab configurations as untrusted input. A topology file can bind host paths into nodes and can run arbitrary commands. Review any topology you did not write before you deploy it as root.
Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Support and Licensing
Containerlab is open-source software distributed under the BSD 3-Clause Licence, Copyright (c) 2021 Nokia. Docker Engine is distributed under the Apache License 2.0. The bundled container images are FRRouting 10.7.0 (GPL-2.0-or-later) and Alpine 3.24.1 (permissive open source). This SecureAnyCloud image bundles the unmodified upstream Containerlab release.
SecureAnyCloud provides the packaging, the Docker Engine integration, the digest-pinned open-source image set, the bundled worked example topology, the per-VM credential automation, the security hardening, this deployment guide, and 24/7 support with a guaranteed 24-hour response SLA.
No proprietary vendor network operating system image is included. If you import one, your use of that image is governed by that vendor's own licence terms.
SecureAnyCloud is not affiliated with or endorsed by the Containerlab project, Nokia, the FRRouting project, Docker, Inc. or any network equipment vendor. All names are marks of their respective owners and are used here only to identify the software.
Deploy on Azure
Find Containerlab on Ubuntu 24.04 LTS on the Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.
Deploy on Azure
Launch Containerlab 0.77.0 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com