Skip to main content
RunBook Academy

Docker & ContainersVII · Networkingmacvlan and ipvlan

macvlan and ipvlan — when you need L2 adjacency

Advanced⏱ ~30 mindockerip

What you'll learn

  • Create a macvlan network bounded so it cannot collide with the LAN DHCP pool
  • Explain why the host cannot reach its own macvlan containers, and work around it
  • Choose between macvlan, ipvlan l2 and ipvlan l3
  • Recognise the failure signature of a switch that blocks MAC spoofing

Prerequisites

Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

macvlan and ipvlan make a container appear as a separate host on the same L2 segment as the host’s physical NIC. macvlan gives each container its own MAC address; ipvlan has the containers share the parent interface’s MAC and distinguishes them by IP.

Both remove NAT entirely. The container is a peer on the LAN, reachable without any port mapping, and it sees real client addresses. That is the attraction. The rest of this lesson is the price.

macvlan

Configuration changecreate a macvlan network
PARENT=eth0

docker network create -d macvlan \
--subnet 192.0.2.0/24 \
--gateway 192.0.2.1 \
--ip-range 192.0.2.192/27 \
--aux-address 'router=192.0.2.1' \
-o parent="$PARENT" \
macvlan-net

docker network inspect macvlan-net --format '{{json .IPAM.Config}}'

A container on this network:

  • Has a MAC address distinct from the host’s, generated by Docker.
  • Holds an IP on the same subnet as the parent interface.
  • Is reachable from the LAN with no port mapping and no -p.
  • Reaches the internet through the LAN gateway, not through the host’s NAT.

Getting the host back onto the segment

Two workarounds, both documented upstream, with different shapes.

Configuration changeworkaround 1 — a macvlan shim on the host
PARENT=eth0
SHIM_ADDR=192.0.2.223/32
CONTAINER_RANGE=192.0.2.192/27

# A macvlan interface belonging to the host, on the same parent
sudo ip link add macvlan-shim link "$PARENT" type macvlan mode bridge
sudo ip addr add "$SHIM_ADDR" dev macvlan-shim
sudo ip link set macvlan-shim up

# Route the container range out of the shim rather than out of the parent
sudo ip route add "$CONTAINER_RANGE" dev macvlan-shim

ping -c1 192.0.2.194

Pick the shim address from inside the block you reserved and add it to --aux-address so Docker never allocates it. Make it persistent through your distribution’s network configuration — as a bare ip link it disappears on reboot, and the failure returns looking brand new.

The second workaround is simpler and often better: attach the container to a user-defined bridge as well as the macvlan network. It then has two interfaces, one on the LAN and one reachable from the host, and the host-side tooling talks to the bridge address.

Configuration changeworkaround 2 — dual-homed container
CONTAINER=lan-svc

docker network create --subnet 172.30.0.0/24 mgmt-net

docker run -d --name "$CONTAINER" --network macvlan-net nginx:1.27-alpine
docker network connect mgmt-net "$CONTAINER"

docker inspect --format \
'{{range $n, $c := .NetworkSettings.Networks}}{{$n}}={{$c.IPAddress}} {{end}}' \
"$CONTAINER"

Promiscuous mode and the switch

macvlan puts several MAC addresses behind one physical port. The upstream requirement is unambiguous: “Your networking equipment needs to be able to handle promiscuous mode, where one physical interface can be assigned multiple MAC addresses.”

On bare metal with a normal switch this is the default behaviour. Three places it is not:

  • Cloud VMs. AWS, GCP and Azure filter frames whose source MAC is not the one assigned to the vNIC. macvlan does not work there.
  • Switch ports with port security configured to a single MAC.
  • Wireless. 802.11 does not carry a second MAC behind an associated station. macvlan on a Wi-Fi parent does not work, and this is a protocol limitation rather than a driver bug.
Read-only / Safewhat the failure looks like
$ CONTAINER=lan-svc

# From the container: outbound works
docker exec "$CONTAINER" ping -c2 192.0.2.1

# From another LAN host: inbound does not
# (run on 192.0.2.30)  ping -c2 192.0.2.194
PING 192.0.2.1 (192.0.2.1): 56 data bytes
64 bytes from 192.0.2.1: seq=0 ttl=64 time=0.43 ms
64 bytes from 192.0.2.1: seq=1 ttl=64 time=0.39 ms

--- 192.0.2.194 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss

Illustrative output

Outbound succeeding while inbound fails is the fingerprint. The container’s ARP replies carry a MAC the switch will not forward, so nothing on the LAN can complete an ARP resolution for it. Check the switch’s MAC address table for the host’s port: if you see one MAC where you expect several, the switch is the problem, not Docker.

802.1Q trunking

Name a VLAN sub-interface as the parent and Docker creates it for you:

Configuration changeone network per VLAN
docker network create -d macvlan \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 \
--ip-range 192.0.2.192/27 \
-o parent=eth0.50 \
vlan50-net

# Docker created the sub-interface
ip -brief link show eth0.50

The host port must be a trunk carrying that VLAN. Docker removes the sub-interface when the network is removed — but only the ones it created, so a hand-made eth0.50 survives and will be reused rather than recreated.

ipvlan

Configuration changeipvlan l2
docker network create -d ipvlan \
--subnet 192.0.2.0/24 \
--gateway 192.0.2.1 \
--ip-range 192.0.2.192/27 \
-o parent=eth0 \
-o ipvlan_mode=l2 \
ipvlan-net

ipvlan_mode accepts l2, l3 and l3s; ipvlan_flag accepts bridge, private and vepa. l2 with the bridge flag is the default and the one to start from.

l3 mode is a different thing entirely. The kernel routes rather than bridges: broadcast and multicast are dropped, ARP does not traverse, and containers use a default route out of their interface with no next hop. It follows that:

  • DHCP does not work — the discover is a broadcast.
  • mDNS, NetBIOS discovery and anything else broadcast-based does not work.
  • The upstream network must have routes to your container subnets, because nothing will ARP for them. That is a router change, not a Docker one.

Choose l3 when you want containers on their own routed subnets and you control the routers. Choose l2 when you want them to look like they are plugged into the same switch.

Comparison

Featuremacvlanipvlan (l2)ipvlan (l3)bridge
Container has own MACyesnonono
Reachable from LAN without publishingyesyesrouted onlyno
Needs multiple MACs per portyesnonono
Works on AWS / GCP / Azurenousuallyusuallyyes
Broadcast / DHCP / mDNS reaches containeryesyesnoyes
Upstream router config requirednonoyesno
NAT for outboundnononoyes
Host can reach its own containersnononoyes

The last row is the one people read after they have already deployed.

Sanity check

  • docker network inspect shows an IPRange narrower than the subnet, and the same range is excluded on the DHCP server.
  • A LAN host that is not the Docker host can reach the container.
  • You have consciously decided how the Docker host reaches the container, and tested it.
  • The parent is the bond or VLAN device that survives a NIC failover, not a member interface.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A macvlan container can reach the gateway and other LAN hosts, and they can reach it. Only the Docker host it runs on cannot ping it. What is wrong?

  2. Q2. A macvlan network is created with `--subnet 192.0.2.0/24` and no `--ip-range`, on a LAN with a DHCP server serving the same subnet. What is the likely outcome?

  3. Q3. Which problem does switching from macvlan to ipvlan l2 actually solve?

  4. Q4. Which are true of ipvlan `l3` mode? Select all that apply.

  5. Q5. On a bonded host, a macvlan network should be parented on the bond device rather than on a member NIC such as eth0.

Passing score: 75%. Answers are checked in this browser.