Skip to content

Parent-attached modes (macvlan and ipvlan)

docker-net-dhcp supports three attachment modes:

mode how containers reach the LAN each child's MAC host changes required
bridge a veth pair plugged into a Linux bridge you maintain, or one the plugin makes from a spare NIC (parent, v2.3.0) random per veth yes: you bring the bridge, or a spare NIC that stays up with no address
macvlan a per-container macvlan child (bridge mode by default) of a host NIC distinct (kernel-generated); the parent's under passthru none, the host NIC is untouched; passthru takes it from the host
ipvlan a per-container ipvlan child (L2 mode) of a host NIC shared with parent none, the host NIC is untouched

Picking between macvlan and ipvlan

Both modes attach directly to a host NIC and require no Linux bridge. The difference is at L2: each container's MAC.

  • macvlan is the default choice. Each container gets its own MAC, which is what most LANs and DHCP servers expect. The Fritz.Box (or any home/SOHO router) sees each container as a fully distinct device.
  • ipvlan is the right pick when the upstream switch or hypervisor refuses to bridge multiple MACs from one port. Common triggers: managed switches with sticky-MAC port-security enabled, Wi-Fi access points that refuse multi-MAC bridging, hypervisor vSwitches with strict port policies. Children share the parent's MAC; the LAN distinguishes containers by IP only.

If macvlan works on your LAN, use macvlan. ipvlan is the escape hatch for hostile L2.

Quick start

Create a network attached to one of the host's NICs (eth0 below; substitute yours, and ip -brief link lists them):

# On arm64 use the -arm64 tag. A network stores this exact reference
# as its driver, so it must name the plugin you installed.
docker network create \
    --driver=ghcr.io/claymore666/docker-net-dhcp:v2.3.1 \
    --ipam-driver=null \
    -o mode=macvlan \
    -o parent=eth0 \
    lan-dhcp

Then attach any container the usual way. No static IP, no labels, no sidecar, no cap_add:

services:
  app:
    image: nginx
    networks: [lan-dhcp]

networks:
  lan-dhcp:
    external: true
docker compose up -d
docker inspect app | jq '.[0].NetworkSettings.Networks'
# IPAddress is the lease your DHCP server handed out

What happens under the hood

  1. docker run triggers libnetwork's CreateEndpoint against the plugin.
  2. The plugin creates a macvlan child on the parent NIC (submode = bridge, so children on the same parent can talk to each other), still in the host netns.
  3. A one-shot DHCP acquisition runs on the new link: DHCPDISCOVER → REQUEST → ACK from your LAN's DHCP server. The lease (IP, mask, gateway) is captured.
  4. The plugin returns the link name to libnetwork via Join. Docker moves the link into the container's netns and renames it (typically eth0).
  5. A persistent DHCP client runs inside the container netns to renew the lease for the lifetime of the endpoint. It is a goroutine in the plugin process and never a child process, and it never configures the link itself: the plugin applies every lease change via netlink.
  6. On docker stop, libnetwork calls Leave → the persistent client is stopped. By default it does not release the lease: the address stays leased until it expires, or until the container comes back and re-claims it, exactly as it would for a physical host that rebooted (v1.9.0+, #800). A network created with release_lease=on_stop hands it back here instead, and gives up the stable MAC and address across a restart to do so (v2.1.1+, #962). A network created with release_lease=on_remove keeps both, and a sweep hands the addresses back about a minute later if no container has claimed them (v2.2.0+, #984).
  7. The macvlan link is reaped automatically when the container netns is destroyed.

The host's NIC config (IP, routes, netplan/systemd-networkd, /etc/network/interfaces) is never touched. The one host link the plugin adds is a vlan sub-interface (below), which it creates, marks and removes itself.

Constraints

  • The parent NIC must support macvlan/ipvlan children. Physical Ethernet, VLAN sub-interfaces, and bonds work; bridges, macvlans, and ipvlans do not (you can't stack these on top of each other).
  • A VLAN of the parent is one option away since v2.3.0: -o parent=eth0 -o vlan=100 attaches the children to eth0.100, which the plugin creates when it is missing and removes with the last network on it, never one it did not create. <parent>.<id> must fit the kernel's 15 bytes. The sub-interface is a parent of its own, so a macvlan network on eth0.100 and an ipvlan network on eth0 do not clash. See VLAN sub-interfaces (#902).
  • The parent NIC must be administratively UP before you create the network. The plugin won't bring it up for you, since host config is off-limits. A vlan sub-interface the plugin creates is brought up by the plugin; its parent must be UP already.
  • Like any macvlan/ipvlan setup: a container on a child interface cannot reach the parent NIC's own host IP, and vice-versa. This is a kernel-level rule and never a plugin restriction. For host↔container traffic you'd need bridge mode or a second NIC.
  • The parent no longer needs an address on the leased subnet for conflict detection. It used to: the plugin checked each new lease against the segment (v1.6.0+, #524) by resolving the address from the parent, and a host answers an ordinary ARP request only if it can route a reply back to the sender, so an address-less parent left the check undetermined. Since 2.0 the DHCP client runs RFC 5227 from inside the container instead, and a §2.1.1 Probe carries an all-zero sender protocol address that Linux answers for any local target without consulting a route. A bare parent is fine. See conflict_check and /Plugin.Health.
  • ipvlan-specific: custom MAC addresses are unsupported (children share the parent's MAC). Passing --mac-address on docker run with an ipvlan network will fail with invalid MAC address.
  • ipvlan-specific: only L2 mode leases. -o ipvlan_mode=l3 and l3s are refused at docker network create, because such a child sends no broadcast and its DHCPDISCOVER reaches no server or relay. -o macvlan_mode= picks bridge, vepa, private or passthru for macvlan. See macvlan and ipvlan sub-modes.
  • ipvlan-specific: if your DHCP server keys reservations solely on MAC and ignores DHCP option 61 (client identifier), ipvlan won't work as a stability mechanism, because every ipvlan slave shares the parent's MAC, so the server has no way to tell them apart. Use mode=macvlan if your server is MAC-only. (See "DHCP identity" below for what the plugin sends.)
  • ipvlan-specific: only one of macvlan or ipvlan can be active on a given parent NIC at a time. The kernel rejects mixing them with EBUSY. Use one mode per parent.
  • Docker's default IPAM is never the allocator: the LAN's DHCP server is the address source of truth. Pass --ipam-driver=null, or, from v2.1.0, this plugin's own IPAM driver (reference). ipvlan takes --ipam-driver=null only: Docker sets the MAC it generates for an IPAM driver on the container's interface at start, and an ipvlan interface cannot change its MAC (#949).
  • One DHCP-served network per container. If a container also joins a bridge or other Docker network, that's its problem to coordinate.

Verifying

After creating a container on the network:

# Container's view
docker exec <container> ip -4 addr show
docker exec <container> ip -4 route show

# Host's view of the lease
docker inspect <container> | jq '.[0].NetworkSettings.Networks'

# Upstream DHCP server's view (Fritz.Box, pfSense, etc.)
# Check the active leases page in your router's UI; the container's
# MAC and hostname should appear.

A container on a macvlan should be pingable from any other host on the LAN on the IP its DHCP server handed it.

If you also want to confirm the lease is not colliding with something already on the segment, read acd_probes_sent before believing address_conflicts_v4 is zero. With no probes the two readings are identical, and "the detector never ran" is what the fault behind #524 looked like. A zero is also the honest answer on a network created with -o conflict_check=off, which asks for no probes at all. Both counters are on /Plugin.Health.

Troubleshooting

"parent interface is unsuitable for macvlan" means you passed a bridge, macvlan, or ipvlan as parent. Use a real NIC, a VLAN sub-interface, or a bond.

"ipvlan does not support a custom MAC address" means docker run --mac-address is not compatible with mode=ipvlan because ipvlan children share the parent's MAC. Drop the --mac-address flag, or switch the network to mode=macvlan if you need distinct MACs.

"parent interface is down" is fixed by ip link set <parent> up. The plugin won't toggle host link state.

Container gets no IP: check that the parent NIC is on the right L2 segment, that DHCP traffic isn't being filtered (some managed switches have DHCP snooping or storm-control turned on), and that the upstream DHCP server has a free lease in its pool. -o validate_dhcp=true catches all three at network-create time instead of at the first docker run.

Everything not specific to these modes (general symptoms, the Compose merge trap, health-endpoint problems) is in the driver reference.

Where the rest of the documentation lives

This page covers choosing and setting up macvlan or ipvlan. The behaviour of the plugin itself is identical in every mode and is documented once, in the driver reference:

For how it works under the hood, see How it works.