A VLAN interface on Ubuntu is one netplan stanza: a VLAN ID, a parent NIC and an address. The kernel adds the 802.1Q tag, the switch port has to accept it, and that is the whole trick. The parts that bite are elsewhere: a two minute boot hang, a firewall that lets ping through and blocks everything else, and links that refuse to go away after you delete them from the config.
This guide creates a VLAN interface on Ubuntu three ways: a temporary one with ip link for testing, a permanent netplan VLAN on a server, and an nmcli VLAN on Ubuntu Desktop. It then routes between two VLANs on an Ubuntu box, bridges a VLAN for KVM guests and removes a VLAN cleanly. Every command below was run on Ubuntu 26.04.1 (kernel 7.0, netplan 1.2) and Ubuntu 24.04.5 (kernel 6.8, netplan 1.1.2) in October 2026.
How the lab fits together
Three Ubuntu hosts share one trunk. Each has a second NIC, ens19, that carries tagged frames only. web01 lives in VLAN 10, db01 in VLAN 20, and gw01 has a leg in both and routes between them.

In the lab, the trunk is a Linux bridge on Proxmox with no VLAN filtering, so it passes every tag between the VMs. On real hardware it is a managed switch port set to trunk (tagged) mode for the VLANs the server needs. If the port is an access port, tagged frames from Ubuntu are dropped and nothing in this guide will work. The switch side of VLANs and trunks covers that end.
| Host | Ubuntu | VLAN interface | Address | Role |
|---|---|---|---|---|
| gw01 | 26.04 | vlan10, vlan20 | 10.10.10.1, 10.20.20.1 | Gateway for both VLANs |
| web01 | 26.04 | vlan10 | 10.10.10.10/24 | Server in VLAN 10 |
| db01 | 24.04 | vlan20 | 10.20.20.10/24 | Server in VLAN 20 |
Check the parent interface
Find the NIC that is plugged into the trunk port. Brief mode keeps it to one line per interface:
ip -br link
On web01 the trunk NIC is ens19. Yours is likely enp1s0, eno1 or similar:
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0 UP bc:24:11:9f:69:92 <BROADCAST,MULTICAST,UP,LOWER_UP>
ens19 UP bc:24:11:31:6d:49 <BROADCAST,MULTICAST,UP,LOWER_UP>
Skip the vlan package that older guides install. It ships the deprecated vconfig tool plus ifupdown hooks, and neither netplan nor ip needs any of it. The 8021q kernel module is not loaded on a fresh install, but the kernel loads it on its own the first time a VLAN link is created. Nothing to modprobe.
One warning before you touch the network over SSH. If the VLAN goes on the NIC that carries your SSH session, a wrong VLAN ID or a port that is not trunked will cut you off. Keep a console open (IPMI, Proxmox, cloud serial console) until the new config is proven.
Create a temporary VLAN interface with ip
A runtime VLAN is the fastest way to prove the switch port really carries the tag. It lives until reboot and leaves no files behind. Set the parent NIC, VLAN ID and address once:
export NIC="ens19"
export VID="10"
export VIP="10.10.10.10/24"
Create the tagged link, give it the address and bring it up:
sudo ip link add link "${NIC}" name "${NIC}.${VID}" type vlan id "${VID}"
sudo ip addr add "${VIP}" dev "${NIC}.${VID}"
sudo ip link set dev "${NIC}.${VID}" up
The detailed link view proves the kernel treats it as an 802.1Q interface with the right ID:
ip -d link show "${NIC}.${VID}" | grep -o "vlan protocol 802.1Q id [0-9]*"
Expected output:
vlan protocol 802.1Q id 10
Ping something that already lives in that VLAN, usually its gateway. In the lab that is gw01 at 10.10.10.1, configured as shown in the inter-VLAN routing section further down. On your network it is the existing switch or firewall interface for that VLAN:
ping -c 3 10.10.10.1
Replies mean the tag made it across the trunk and back:
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=0.648 ms
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=0.238 ms
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=0.333 ms
--- 10.10.10.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2037ms
A wrong VLAN ID gives no error at all. We moved the same address onto a VLAN 11 link while the gateway stayed on VLAN 10, and the result was plain 100% packet loss with nothing in the logs. When ping fails silently, check the ID on both ends before anything else.
Delete the test link before you make the VLAN permanent, so netplan starts from a clean slate:
sudo ip link delete "${NIC}.${VID}"
Make the VLAN permanent with netplan
Ubuntu Server renders netplan into systemd-networkd files, so a netplan VLAN survives reboots with no extra service. Put VLANs in their own file next to 50-cloud-init.yaml instead of editing that one, because cloud-init can rewrite it:
sudo vim /etc/netplan/60-vlans.yaml
This is the exact file from web01. Change ens19, the ID, the address and the route to match your network:
network:
version: 2
ethernets:
ens19:
dhcp4: false
dhcp6: false
optional: true
vlans:
vlan10:
id: 10
link: ens19
addresses:
- 10.10.10.10/24
routes:
- to: 10.20.20.0/24
via: 10.10.10.1
The parent must appear under ethernets, even with no address. Leave it out and netplan generate stops with Error in network definition: vlan77: interface 'ens19' is not defined. The interface name (vlan10 here) is free text up to 15 characters. The route sends the subnet of the other VLAN through the VLAN 10 gateway while the default route stays on the main NIC. Do not drop optional: true: the next section shows the boot delay you get without it.
Lock the file down, then generate and apply:
sudo chmod 600 /etc/netplan/60-vlans.yaml
sudo netplan generate
sudo netplan apply
Skip the chmod and every netplan command prints this, several times per apply:
** (configure:28034): WARNING **: 20:53:04.155: Permissions for /etc/netplan/60-vlans.yaml are too open. Netplan configuration should NOT be accessible by others.
The config still applies, but the file may hold Wi-Fi keys or other secrets later, so 600 is the right mode. On a remote box where the VLAN carries your session, run sudo netplan try instead of apply. It applies the change and rolls it back after 120 seconds unless you press Enter, which saves you from a bad VLAN ID.
Check the result as networkd sees it:
ip -br addr show type vlan
networkctl list
The VLAN should be routable and configured. The parent shows degraded because it has only an IPv6 link-local address, which is expected for a trunk-only NIC:
vlan10@ens19 UP 10.10.10.10/24 fe80::be24:11ff:fe31:6d49/64
IDX LINK TYPE OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 eth0 ether routable configured
3 ens19 ether degraded configuring
6 vlan10 vlan routable configured
4 links listed.
Netplan wrote one .netdev and one .network file per VLAN under /run/systemd/network/. Those are generated on every boot from your YAML, so never edit them directly.
Why “optional: true” matters: the 2 minute boot hang
Leave optional: true off the parent and the VLANs still come up, but every reboot takes more than two minutes on both releases. With the parent not optional, systemd-analyze blame names the culprit:
2min 45ms systemd-networkd-wait-online.service
sudo journalctl -b -u systemd-networkd-wait-online shows the service giving up exactly two minutes after it started:
Oct 07 21:14:10 gw01 systemd[1]: Starting systemd-networkd-wait-online.service - Wait for Network to be Online...
Oct 07 21:16:10 gw01 systemd-networkd-wait-online[885]: Timeout occurred while waiting for network connectivity.
Oct 07 21:16:10 gw01 systemd[1]: systemd-networkd-wait-online.service: Main process exited, code=exited, status=1/FAILURE
Oct 07 21:16:10 gw01 systemd[1]: systemd-networkd-wait-online.service: Failed with result 'exit-code'.
Oct 07 21:16:10 gw01 systemd[1]: Failed to start systemd-networkd-wait-online.service - Wait for Network to be Online.
The parent NIC has no address, so networkd leaves it in the configuring state forever, and wait-online waits for every required link until it times out. Marking the parent optional tells networkd not to wait for it. The netplan YAML reference documents the key. Measured on the same VMs:
| Host | Without optional: true | With optional: true |
|---|---|---|
| gw01 (26.04, two VLANs) | 2 min 10.2 s | 11.4 s |
| web01 (26.04) | 2 min 10.1 s | 10.1 s |
| db01 (24.04) | 2 min 8.1 s | 11.3 s |
Route between VLANs on Ubuntu
Hosts in different VLANs cannot reach each other until something routes between them. Usually that is a layer 3 switch or a firewall. An Ubuntu box with one trunk NIC can do the same job as a router on a stick, the Linux version of the Cisco router-on-a-stick setup. gw01 gets one VLAN interface per network, and each address becomes the gateway for that VLAN:
sudo vim /etc/netplan/60-vlans.yaml
Two VLANs on the same parent, no routes needed because both subnets are directly connected:
network:
version: 2
ethernets:
ens19:
dhcp4: false
dhcp6: false
optional: true
vlans:
vlan10:
id: 10
link: ens19
addresses:
- 10.10.10.1/24
vlan20:
id: 20
link: ens19
addresses:
- 10.20.20.1/24
Apply it the same way. IP forwarding is off by default on Ubuntu (net.ipv4.ip_forward = 0), and with it off, a ping from web01 to db01 got 100% loss. Turn it on persistently:
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-vlan-routing.conf
sudo sysctl --system
From web01, trace the path to db01:
tracepath -n 10.20.20.10
Two hops, with the VLAN 10 gateway first:
1?: [LOCALHOST] pmtu 1500
1: 10.10.10.1 0.382ms
1: 10.10.10.1 0.320ms
2: 10.20.20.10 0.669ms reached
Resume: pmtu 1500 hops 2 back 2
A capture on gw01’s trunk NIC shows the routing at layer 2. The request arrives tagged VLAN 10 and leaves tagged VLAN 20, and the reply makes the same trip back:

Run the same capture on your own trunk NIC whenever you doubt the tags:
sudo tcpdump -i ens19 -e -nn -q -c 4 "vlan and icmp"
If it shows no vlan lines at all, the frames never reached the box.
UFW lets ping through but blocks TCP between VLANs
This one cost us a few minutes. With UFW enabled on gw01 (default policy deny (routed)), ping between the VLANs still worked, so routing looked fine. SSH from web01 to db01 timed out:
nc: connect to 10.20.20.10 port 22 (tcp) timed out: Operation now in progress
Ping gets through because /etc/ufw/before.rules accepts forwarded ICMP echo requests before your policy applies. TCP hits the routed deny, which the kernel log shows:
[UFW BLOCK] IN=vlan10 OUT=vlan20 ... SRC=10.10.10.10 DST=10.20.20.10 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=9435 DF PROTO=TCP SPT=57220 DPT=22 WINDOW=64240 RES=0x00 SYN URGP=0
Allow forwarding between the two VLAN interfaces explicitly:
sudo ufw route allow in on vlan10 out on vlan20
sudo ufw route allow in on vlan20 out on vlan10
After that, nc -zv -w 3 10.20.20.10 22 from web01 printed Connection to 10.20.20.10 22 port [tcp/ssh] succeeded!. In production, narrow these rules to the ports each VLAN actually needs (... out on vlan20 to any port 5432 proto tcp) rather than opening everything. The UFW firewall guide covers the rule syntax.
Put the default gateway on the VLAN
When the VLAN is the main network of the server, it carries the default route and the DNS servers. The via address must be a router that already reaches the internet, normally the L3 switch or firewall for that VLAN. In the lab, gw01 played that role with a masquerade rule on its uplink. A reboot dropped that rule. db01 kept its default route and lost the internet.
Netplan expects a single default route per address family unless you add metrics or routing policy, so remove the old one first. On cloud images it lives in 50-cloud-init.yaml. cloud-init generated that file and can regenerate it, so stop cloud-init from managing the network before you edit it:
echo "network: {config: disabled}" | sudo tee /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
Then delete the routes entry for default from /etc/netplan/50-cloud-init.yaml. If that interface uses dhcp4: true instead, there is no static route to delete; add dhcp4-overrides: {use-routes: false} under it so DHCP stops installing the default route. The static-route edit on db01 survived a reboot with that file in place. Moving the default route can drop SSH sessions from other subnets. Work from a console or use netplan try for this change. This is the VLAN file from db01:
sudo vim /etc/netplan/60-vlans.yaml
The VLAN gets the default route via its gateway, plus resolvers:
network:
version: 2
ethernets:
ens19:
dhcp4: false
dhcp6: false
optional: true
vlans:
vlan20:
id: 20
link: ens19
addresses:
- 10.20.20.10/24
routes:
- to: default
via: 10.20.20.1
nameservers:
addresses:
- 1.1.1.1
- 9.9.9.9
Set chmod 600 on the file, run sudo netplan try and press Enter while the session still answers. Then confirm the route and DNS landed on the VLAN:
ip route get 1.1.1.1
resolvectl dns vlan20
Outbound traffic now leaves through vlan20, and systemd-resolved uses the two resolvers from the file:
1.1.1.1 via 10.20.20.1 dev vlan20 src 10.20.20.10 uid 1000
cache
Link 4 (vlan20): 1.1.1.1 9.9.9.9
The same checks from a fresh session on db01, with the tag ID and a ping to the internet through the VLAN gateway:

For a plain untagged static address with no VLAN involved, the netplan static IP guide has the simpler file.
Bridge a VLAN for KVM virtual machines
To put VMs on a VLAN, attach the VLAN interface to a bridge and move the host address to the bridge. On web01 the VLAN 10 address moved to br10:
sudo vim /etc/netplan/60-vlans.yaml
The VLAN keeps no address of its own. The bridge holds the address and route:
network:
version: 2
ethernets:
ens19:
dhcp4: false
dhcp6: false
optional: true
vlans:
vlan10:
id: 10
link: ens19
bridges:
br10:
interfaces:
- vlan10
addresses:
- 10.10.10.10/24
routes:
- to: 10.20.20.0/24
via: 10.10.10.1
parameters:
stp: false
forward-delay: 0
Use netplan apply for this one. netplan try refuses any config with bridge parameters:
br10: reverting custom parameters for bridges and bonds is not supported
Please carefully review the configuration and use 'netplan apply' directly.
After apply, bridge link show lists the VLAN as a forwarding port of the bridge. It stayed that way after a reboot (8.6 s boot):
5: vlan10@ens19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br10 state forwarding priority 32 cost 100
A guest attached to br10 sends untagged frames, and the host adds the VLAN 10 tag on the way out of ens19. One bridge per VLAN scales fine for a handful of networks. The Linux bridge on VLAN with netplan guide goes further with several VLAN bridges, and the KVM bridged network setup wires the bridge into libvirt.
Add a VLAN with nmcli on NetworkManager hosts
Ubuntu Desktop hands every interface to NetworkManager, so nmcli is the natural tool there. One command creates and stores the VLAN connection. We ran it on Ubuntu 24.04 with NetworkManager as the netplan renderer, which is how Desktop is set up:
sudo nmcli connection add type vlan con-name vlan30 ifname vlan30 dev ens19 id 30 ipv4.method manual ipv4.addresses 10.30.30.10/24 ipv6.method disabled
sudo nmcli connection up vlan30
Running the same command on Ubuntu Server, after installing network-manager, creates the connection but fails to activate it:
Error: Connection activation failed: Activation failed because the device is unmanaged
Ubuntu ships /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf with unmanaged-devices=*,except:type:wifi,..., so NetworkManager ignores wired NICs unless netplan hands them over. Desktop installs do that with a one-line renderer file. On a server, stick with the netplan file above. The same file can switch the whole host to NetworkManager. Do that from a console, not SSH. Right after netplan apply, nmcli answered Error: NetworkManager is not running. while the service restarted.
sudo vim /etc/netplan/01-network-manager-all.yaml
The content is the same as on a desktop install:
network:
version: 2
renderer: NetworkManager
After chmod 600 and netplan apply, netplan writes an empty override to /run/NetworkManager/conf.d/, the devices become managed, and nmcli connection up vlan30 returned Connection successfully activated.
On Ubuntu 24.04, NetworkManager does not keep the connection in its own keyfile directory. It writes it back into netplan as /etc/netplan/90-NM-<uuid>.yaml, so ls /etc/netplan/ shows nmcli-made VLANs too:
network:
version: 2
vlans:
vlan30:
renderer: NetworkManager
addresses:
- "10.30.30.10/24"
id: 30
link: "ens19"
networkmanager:
uuid: "4c1667ba-50e3-4cc8-b26c-2a5bcf032b8f"
name: "vlan30"
passthrough:
ethernet._: ""
vlan.flags: "1"
ipv6.addr-gen-mode: "default"
ipv6.method: "disabled"
ipv6.ip6-privacy: "-1"
proxy._: ""
Change NM-managed VLANs with nmcli, not by editing that file, or the two views drift apart. Other distros use different tools for the same job: see the Debian VLAN guide for ifupdown and the RHEL and Fedora VLAN tagging guide for nmcli on that family.
Remove a VLAN interface cleanly
Deleting a VLAN from the YAML and running netplan apply does not delete the interface. We added a vlan99 stanza, applied, removed it and applied again, then checked both views:
ip -br addr show type vlan
networkctl list | grep vlan99
The address went away, but the link stayed up and networkd marked it unmanaged:
vlan10@ens19 UP 10.10.10.10/24 fe80::be24:11ff:fe31:6d49/64
vlan99@ens19 UP fe80::be24:11ff:fe31:6d49/64
5 vlan99 vlan carrier unmanaged
netplan apply creates virtual devices but does not remove them. A reboot clears the leftover, or delete it by hand right after the config change:
sudo ip link delete vlan99
nmcli behaves the same way. sudo nmcli connection delete vlan30 removed the profile and its 90-NM-*.yaml file, but vlan30@ens19 was still listed by ip -br link show type vlan, so remove it with the same ip link delete. Finish by confirming only the VLANs you want are left:
ip -br link show type vlan
A stale VLAN with no address does no harm on its own. It does confuse the next person who reads ip a and assumes the config file matches the running system, so clean it up in the same change window. If the switch side is also being retired, remove the VLAN from the allowed list on the trunk there; when you are shopping for that hardware, managed switches for a homelab lists models that handle 802.1Q trunks properly.
How do I ping from the vlan interface
ping -I vlan30 http://www.google.com
This information is out of date. A fresh install of ubuntu 24.10 is not using the /etc/network/interfaces file to bring up networking anymore.
depends what you use to install later.. i do not prefer netplan so i removed it and installed interfaces.