docker-net-dhcp¶
A Docker network plugin that leases every container's address from the
DHCP server your LAN already runs, over bridge, macvlan or ipvlan,
for IPv4 and IPv6. One docker network create, then networks: [lan-dhcp]
in any Compose file. No static addresses, no sidecars, no plumbing per
container.
The problem it solves: with plain macvlan or static addressing, a
container can come back from an update or a restart with a different
address, or two containers can end up on one address. This plugin leases
each address from the LAN's DHCP server and, in bridge and macvlan
mode, keeps one identity per container across restarts and upgrades, so
the router's lease table and its reservations stay true.
Every release is run in a separate lab against real DHCP servers, Kea (single, hot-standby pair and behind a relay), ISC dhcpd (single and failover pair), dnsmasq, udhcpd, Pi-hole and OpenWrt, with Docker hosts on Debian 13, Ubuntu 24.04 and Debian 11 with Docker 20.10; a MikroTik RouterOS CHR cell is set up but has written no verdicts yet (latest lab results, how the lab works).
This is the successor of devplayer0/docker-net-dhcp, not a patched copy:
2.x has its own DHCP engine and its own lifecycle code.
This documentation is the 2.x line's
These pages describe the 2.x build. The snippets install the current release. Pick a v1.x version from the selector for the 1.x manual. The 1.x line is end of life and gets no further releases, security fixes included.
sudo mkdir -p /var/lib/net-dhcp # once per host
docker plugin install ghcr.io/claymore666/docker-net-dhcp:v2.7.0 # -arm64 on arm64
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
--ipam-driver null -o mode=macvlan -o parent=eth0 lan-dhcp
docker run --rm -ti --network lan-dhcp alpine ip address show
The parts and what passes between them are drawn in Architecture.
It is a privileged plugin: it runs with host networking, the Docker socket
and CAP_NET_ADMIN. The full list is under Requirements,
and what each item is for is in SECURITY.md.
Feature list¶
- The address is the LAN's. The router's lease table, its MAC
reservations and, with
-o register_dns=true, its DNS see the container as one more host. - IPv6 in the same shape as IPv4.
-o ipv6_mode=picks DHCPv6, SLAAC, or whatever the router advertisement says, per network, with the same identity rules and the same counters.-o ipv6_pd=64also asks for a delegated prefix, for a container that routes one. A network created with--ipv4=false(Docker 28 or later) runs no DHCPv4, and its containers get IPv6 only (#1135). - One identity per container, kept across restarts. In
bridgeandmacvlanthe plugin keeps the MAC, and with it the DHCP client id and the DHCPv6 DUID, acrossdocker restart, a daemon restart and a plugin upgrade, and asks for the old address again. A MAC reservation on the router keeps matching. - ipvlan gets its own client id. ipvlan containers share the parent's
MAC, so each one gets its own DHCP client id (option 61) and its own
DUID (#895). That identity is new on every restart, so by default an
ipvlan container does not keep its address across
docker restart. Theclient_iddriver option gives a container its own client id that keeps the address across a restart and a recreate (#219). - VLANs, sub-modes, MTU.
-o vlan=puts a macvlan or ipvlan network on a tagged VLAN off the parent (#902);-o macvlan_mode=and-o ipvlan_mode=pick the kernel mode of each link (#905);-o mtu=sets the link MTU (#1037). - Bridge mode from a spare NIC. With
-o parent=the plugin makes the bridge, enslaves the NIC and removes both with the network (#903);-o force_create=trueoverrides the firewall check on that bridge.-o link_local_fallback=truestarts a container on a 169.254 address while the DHCP server is down and moves it to the lease when one arrives (#904). - Leases follow Docker's lifecycle. Renewal, rebind, NAK and expiry run
in the plugin, one client per endpoint.
-o release_lease=hands the address back at stop or at remove. Every Docker hook has a defined outcome, including a container that leaves before its lease binds, a network removed together with its bridge, and an endpoint Docker forgot. - Measured on Docker Engine 20.10 through 29. The engine matrix builds
the plugin, creates networks in every documented shape, confirms a lease
in the DHCP server's own log and keeps an address across
docker restart, on every engine line, weekly and on every change to the measurement. Below 20.10 the plugin refuses to start and names the minimum.
Two shapes¶
| You want | Use |
|---|---|
| The container takes its address from the LAN, Docker keeps no pool | --ipam-driver null (the default shape, all three modes) |
Docker knows the address: docker run --ip, Compose ipv4_address, docker inspect |
the plugin as IPAM driver as well (--ipam-driver <plugin>), bridge and macvlan, IPv4 and IPv6; --ip6 and ipv6_address are served when an IPv6 --subnet is typed and ipv6_mode is dhcp (#1132) |
ipvlan |
--ipam-driver null |
| IPv4 | IPv6 | as IPAM driver | |
|---|---|---|---|
| bridge | yes | yes | yes |
| macvlan | yes | yes | yes |
| ipvlan | yes | yes | no, needs a Docker change (#949) |
Unsupported¶
ipvlanwith this plugin as IPAM driver. Docker assigns a MAC to the ipvlan interface, and ipvlan interfaces cannot take one. Needs a change in Docker, not planned (#949).ipvlannetworks use--ipam-driver null.- Docker Engine 19.03. Unmeasured, not planned.
- IPv6-Only Preferred (DHCPv4 option 108, RFC 8925) is never requested. A Docker endpoint on a network with an IPv4 pool must hold an IPv4 address, which makes it an IPv4-requiring host, and RFC 8925 section 3.2 forbids such a host from asking for the option. The DHCP library implements the option for consumers that are not (#1027).
The plugin refuses these with a message that names the reason.
Planned¶
v2.7.0 adds a client_id driver option for one container (#219), an
EBUSY error that names the children the plugin holds on the parent
(#1372) and a SLAAC address that leaves no kernel prefix route behind
(#1412). v2.8.0 is planned: an IPAM-mode restart that keeps the
address on a DHCP server that files leases by MAC (#1416) and a fix
for renewal behind a DHCP relay (#1419)
(milestone).
The full list,
with what this project will not do, is on the roadmap.
How to check any of this¶
- The DHCP exchange runs inside the plugin on the project's own library, dhcp-golib. No external DHCP client, no client process per container.
- Tests assert on the wire and on the server: packet captures and the DHCP server's lease log, not the plugin's own counters (how this plugin is tested).
- Every pull request carries a public review verdict, and every release ships signatures, SLSA provenance and an SBOM (verifying releases).
Requirements¶
- Docker Engine 20.10 or newer. 20.10 is the lowest version this
plugin is measured on. The engine matrix drives the whole baseline on
every engine line from 20.10 to the current release: the plugin
created from a local build and enabled,
docker network createin bridge, macvlan and ipvlan mode, a lease confirmed in the DHCP server's own log, and an endpoint that keeps its address acrossdocker restart. Pulling the published plugin from a registry is not part of that measurement. It runs weekly and on every change to the measurement. Below 20.10 the plugin refuses to start, and the refusal names the minimum and the engine it saw; an engine older than 19.03 is named by the API version it reported. 19.03 isunsupportedbecause it is unmeasured: on a cgroup v2 host it cannot start a container at all, so nothing there tests this plugin. Every change is also tested against the engine the integration suite runs on, 29.9.2 today, read from the engine drift check in that run'sPrepare the lanestep. - Plugin interface
docker.networkdriver/1.0, which is what the plugin manifest declares. The plugin negotiates the Docker API version with the daemon. It publishes both numbers on/Plugin.Healthasengine_versionandapi_version. The minimum above is a version of the engine, not of the API. The one refusal on the API version is below 1.40, the oldest the plugin's Docker client speaks, which only engines older than 19.03 report. - One directory, created once per host, before
docker plugin install(the line is in the quick start below). Docker will not create a missing bind source, so without it the install fails at start-up and leaves the plugin installed but disabled, after which the identical command answers onlyplugin ... already existsand names nothing. Recovery: the reference. - Architecture.
linux/amd64on the bare tag,linux/arm64on the-arm64tag. A Docker plugin cannot be installed from a multi-architecture manifest list, so the tag is how the architecture is chosen, in every snippet that names the image and not only the install line. Why, in full: Install, upgrade, uninstall. - Privileges. The manifest asks for
hostnetworking, the host PID namespace, the Docker socket, a bind mount of the state directory, a read-only bind mount of/var/run/docker, andCAP_NET_ADMIN,CAP_NET_RAW,CAP_SYS_ADMIN,CAP_SYS_PTRACE.docker plugin installprompts for the set; what each is for is in SECURITY.md. - Kernel link types. Bridge mode needs
vethandbridge;macvlanandipvlaneach need the kernel module of the same name. A stock distribution kernel loads one the first time that link type is asked for, so there is normally nothing to do: measured on Linux 6.12,ipvlanwas absent fromlsmodbefore the firstip link add ... type ipvlanand present after, with nomodprobe. A kernel built without the type, or a host where module loading is turned off, failsdocker network createfor that mode. - Root on the host. The plugin runs as root and its socket lives
under
/run/docker/plugins, which only root can read. Reading/Plugin.Healththerefore needssudo. Without itcurl -sprints nothing and exits 7, which is what an absent socket also gives, so a permission problem looks like a stopped plugin. - Mode constraints.
bridgeexpects a host bridge you maintain, or makes one from a spare NIC that stays up with no address (v2.3.0);macvlanandipvlanattach to a host NIC and change nothing on it, at the cost of the kernel rule that a child cannot reach its own host's address. With-o vlan=<id>(v2.3.0) they attach to a VLAN sub-interface of that NIC, which the plugin creates when it is missing and removes with the last network on it. Both in macvlan / ipvlan modes.
Quick start¶
# Once per host, before the install. See Requirements above.
sudo mkdir -p /var/lib/net-dhcp
# amd64
docker plugin install ghcr.io/claymore666/docker-net-dhcp:v2.7.0
# arm64
docker plugin install ghcr.io/claymore666/docker-net-dhcp:v2.7.0-arm64
One network, created once. macvlan needs only a host NIC; bridge
wants a bridge you bring yourself (Bridge mode):
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
--ipam-driver null -o mode=macvlan -o parent=eth0 lan-dhcp
docker run --rm -ti --network lan-dhcp alpine ip address show
--ipam-driver null stops Docker handing out addresses that would
collide with the real LAN. From v2.1.0 there is a second supported
shape: name the plugin again in place of null and the leased address
goes into Docker's own address management, which makes --ip and
Compose's ipv4_address work.
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
--ipam-driver ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
-o mode=macvlan -o parent=eth0 lan-dhcp
One of the two is required. On arm64 the -arm64 tag goes in these
lines too, because a network records the tagged reference as its driver.
Add -o ipv6_mode=dhcp for a DHCPv6 lease beside the v4 one, or
-o ipv6_mode=slaac to take the address from the router's
advertisement; auto reads the advertisement and does what it says,
DHCPv6 where it asks for DHCPv6 and the prefix where it does not. -o
ipv6=true is the short spelling of dhcp. They work on both lines.
On the IPAM line --ipv6 needs a typed IPv6 --subnet <prefix> (a /126
or shorter) and -o ipv6_mode=dhcp: the plugin allocates no IPv6 pool of
its own, so the subnet is the pool Docker keeps its books in (#1132).
The DHCPv6 server picks each address, and --ip6 asks it for one; a
reply naming another address than --ip6, or one outside the subnet, is
refused and released, and the container does not start. Without a subnet
--ipv6 is refused
(#960), and -o ipv6=true or -o ipv6_mode=<mode> switches IPv6 on
instead. The modes are set out in
the driver reference, and
the two shapes in the same page.
After that, plain Compose. No static addresses, no sidecar, nothing per container:
Documentation¶
- Driver reference is the manual: every option, setting and counter, install and upgrade, lease behaviour, observability, Compose usage, troubleshooting.
- Bridge mode is the one-time host bridge setup.
- macvlan / ipvlan modes covers choosing between them, and their constraints.
- Verifying releases covers signatures, SLSA provenance, SBOMs, and rebuilding the binaries yourself.
- How it works is the mechanism, for contributors.
- How this plugin is tested is what is tested, where, and what each result proves.
- Roadmap is where this is going, and what it will not do.
- Contributing is what an acceptable pull request looks like.
- Release runbook is the maintainer-facing publish procedure.
These pages are versioned: use the selector in the header to read the documentation matching the plugin version you have installed.
Images and releases¶
Images go to GHCR (ghcr.io/claymore666/docker-net-dhcp:vX.Y.Z, primary)
and are mirrored to Docker Hub under two names,
claymore666/net-dhcp:vX.Y.Z and
claymore666/docker-net-dhcp:vX.Y.Z. The two Hub names are the same
image at the same digest; install from either. Pin a version for
reproducibility. What a major, minor or patch release may change is in
Versioning.
Published builds are linux/amd64 on the bare tag and
linux/arm64 as :vX.Y.Z-arm64 / :latest-arm64 (v1.7.0 onward).
Those two are the whole set: 32-bit ARM is not built, so there is no
armv7 or armhf tag to install.
The architecture lives in the tag because a Docker plugin cannot be
installed from a multi-architecture manifest list at all: the daemon
reads a plugin's privileges before pulling it, its manifest handler
matches single manifests only, and an index therefore fails with did not
find plugin config for specified reference on every architecture, with
no --platform to steer it. The -arm64 tag replaces the bare one in
every snippet that names the image, including
docker network create -d: a network records the tagged reference as its
driver, so a bare tag there names a plugin the host does not have.
- GHCR package
- GitHub Releases carries per-release notes, credits and signed artifacts.
Origin and licence¶
This project is the successor of
devplayer0/docker-net-dhcp
(quiet since 2021); since 2.0 it runs its own DHCP engine.
GPL-3.0. See LICENSE.md. The upstream project is GPL-3.0 and this derivative stays under the same licence.
Verifying releases¶
Every release from v1.1.0 is cosign-signed (keyless) on both registries
and ships an SBOM; SLSA build provenance is attested for the GHCR image
only, so verify provenance against the ghcr.io reference. Both
commands need cosign v3 or newer. v2 cannot read the bundle format
the release signs with, and it fails in a way that looks like a broken
signature. Replace VERSION:
cosign verify ghcr.io/claymore666/docker-net-dhcp:VERSION \
--certificate-identity-regexp '^https://github.com/claymore666/docker-net-dhcp/.github/workflows/release.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/claymore666/docker-net-dhcp:VERSION --repo claymore666/docker-net-dhcp
The whole procedure, including rebuilding the binaries yourself, is in Verifying releases.
Project & community¶
- Contributing: open a pull request against the
devbranch. See Contributing. - Security policy / vulnerability reporting: SECURITY.md. Do not open a public issue for a vulnerability.
- Bug reports & feature requests: the issue forms.