Skip to content

Roadmap

Where docker-net-dhcp is going over roughly the next year, and, the more useful half, where it deliberately is not going.

This page is direction, not a delivery schedule. The project is solo-maintained, so nothing here carries a date. The milestone links below decide what is in a release; this page follows them.

Milestones

Release State Theme Milestone
v2.3.0 released The host plumbing an operator does by hand today, and the gaps the IPAM shape still refuses milestone 31
v2.3.1 released IPv6 routes at Join, the fixed-MAC hand-over in IPAM mode, and diagrams of the plugin and the lab milestone 38
v2.4.0 released The rest of IPv6, and the DHCP options the client does not read yet milestone 34
v2.5.0 released CI consolidation and code debt, and DHCPv6 prefix delegation milestone 35
v2.6.0 released Networks without an IPv4 pool, an IPv6 pool in the IPAM shape, and the CI consolidation milestone 39
v2.6.1 released IPv6 address lifetimes and removal conditions, and the Docker Hub description check milestone 41
v2.7.0 released A per-container client_id option, the EBUSY error that names the children, and the SLAAC prefix route milestone 42
v2.8.0 planned An IPAM-mode restart that keeps the address on a server that files leases by MAC, and renewal behind a DHCP relay milestone 43

v2.3.0, released

  • #902, a vlan option that puts the network on a tagged VLAN off the parent
  • #903, a bridge the plugin creates and owns
  • #904, link-local fallback where no server answers, off by default
  • #905, macvlan and ipvlan sub-mode options
  • #960, IPv6 in IPAM mode: the v6 exchange, the v6 record, and a DUID that survives a restart
  • #1029, the DHCPv6 Client FQDN option, so register_dns registers the AAAA record beside the A; without it a DHCPv6 lease carries no name
  • #1036, a require_mac option that refuses an endpoint with no Docker-supplied MAC
  • #1037, an mtu option that sets the endpoint link MTU explicitly
  • #1045, the code-scanning result is a required check on both protected branches
  • #1096, a page that says how the plugin is tested, linked from the README
  • #1108, the first screen of the README says what, how and why

v2.3.1, released

  • #1118, in IPAM mode the kept identity of a stopped container goes back to the container with that MAC, and with require_mac to no other
  • #1125, Join returns each IPv6 route once, an endpoint with no IPv6 address no longer crashes it, and ipv6_mode alone gets the IPv6 default route
  • #1126, an architecture page with a diagram and a picture of the real-server lab in the testing page

v2.4.0, released

  • #1028, PREF64 from the Router Advertisement, the NAT64 prefix
  • #1030, Microsoft classless static routes, option 249, where 121 is absent. The library half comes with the dhcp-golib v1.3.0 pin (#1157): option 249 directly after 121 in the request list, and its routes read when 121 is absent; what stays is the plugin's [Join] log line naming the option and its test against a server that sends 249 alone
  • #1031, DHCPv4 Rapid Commit
  • #1119, DHCPv4 FORCERENEW with nonce authentication (RFC 3203, RFC 6704): the plugin's own log lines and health counters. The library half comes with the dhcp-golib v1.2.0 pin (#1137): option 145 in every Discover and Request, and a renewal on an authenticated DHCPFORCERENEW, with any other discarded and counted in the library's statistics
  • #1120, user_class=, the DHCPv4 User Class option 77 (RFC 3004)
  • #1032, ipv6_iid=stable-privacy, the RFC 7217 interface identifier, with modified EUI-64 kept as the default. The library half comes with the dhcp-golib v1.4.0 pin (#1177): Params6.IID, off by default; what stays is the plugin's option and the identifier inputs it passes
  • #1033, the DHCPv6 timezone options logged as the v4 ones are. The library half comes with the v1.4.0 pin (#1177): the Reply's options on the lease
  • #859, the whole DHCPv6 NTP server list, option 56, in the same log line, one entry per instance
  • #1034, the DHCPv4 vendor-specific options 43 and 125 logged. The DHCPv6 half's library side comes with the v1.4.0 pin (#1177): option 17 on the lease
  • #1038, an ipv6_mode=auto network remembers a silent DHCPv6 server for DHCPV6_ABSENCE_MEMORY (default ten minutes, 0 turns it off): further endpoints form their address from the advertised prefix without soliciting, counted in dhcpv6_absence_remembered; a granted DHCPv6 address, removing the network or a restart clears it
  • #926, DHCPv6 Rapid Commit, the two-message exchange
  • #927, DHCPv6 temporary addresses (IA_TA)

v2.5.0, released

  • #214, DHCPv6 prefix delegation (IA_PD): -o ipv6_pd=<length> asks the server for a prefix beside the address and installs it in the container as one unreachable route, for software there that routes; the Kea DHCPv6 test fixture shipped in v2.4.0
  • #1203, the DHCPv6 option 17 logged as vendor_17, the plugin half of #1034
  • #744, one subject discovery, one refusal and one collation in a shared shell library
  • #745, five gates merged into the gates that read the same files, and the release preflight taken out of the gate class
  • #746, the four integration lanes reduced to one reusable workflow
  • #747, four detectors modelling one vendor scheduler collapsed into one
  • #748, the process tier trimmed and frozen until there is a second maintainer
  • #749, gate expiry, the premise rule turned on the gates themselves
  • #798, a publisher outside the standard build-and-push shape is dropped from a gate's population
  • #799, the release runbook read against the release workflow by a gate, and the arm64 install proofs no longer wait on the amd64 build
  • #856, a gate that fails CI when a changed or removed metric, health key or ledger kind is missing from the unreleased release notes
  • #861, a gate that fails the lane when a Go doc block opens by naming a different Test function
  • #886, the runner-pool facts file compared with the registered runners daily
  • #680, a host AppArmor profile silently disables the kea fixture
  • #690, what each capability in config.json buys, measured by a matrix job
  • #657, the netlink seam enforced, so the endpoint paths can be unit-tested
  • #674, what a version number promises, written down
  • #178, the frozen docker/docker module replaced by the ones moby publishes

v2.6.0, released

  • #1135, networks created with --ipv4=false (Docker 28 or later): no DHCPv4 and no IPv4 address on the endpoint, IPv6 only
  • #1132, an IPv6 pool from a typed IPv6 --subnet in the IPAM shape, which serves --ip6 and --ipv6
  • #1288, release_lease sends the IPv4 release from a parent that holds no IPv4 address
  • #733, the CI consolidation: one preflight and one suite runner for the four integration lanes

v2.6.1, released

  • #1369, an ipv6_mode=auto endpoint that fell back to the advertised prefix gets that prefix's lifetimes at once and no longer solicits the silent DHCPv6 server again
  • #1368, the reference names when the plugin removes an IPv6 address; it does not remove one because the router stopped advertising the prefix
  • #1371, the reference says why a DHCPv6 server logs a Confirm after the Reply that granted the lease
  • #1370, a test pins proto dhcp on the delegated-prefix route
  • #1324, each release run reads both Docker Hub descriptions back after the sync and goes red when either differs

v2.7.0, released

  • #219, a client_id driver option that sets DHCP option 61 for one container, so ipvlan containers keep their address across a restart and a recreate
  • #1372, the EBUSY error for a macvlan or ipvlan child names the children the plugin holds on the parent
  • #1412, a SLAAC address leaves no kernel prefix route behind after the router withdraws its prefix
  • #1406, the plugin image drops five apk pins that Alpine 3.24.3 makes redundant
  • #1417, the README, the docs index and the testing guide name what the real-server lab covers

v2.8.0, planned

  • #1416, IPAM mode: a restarted container keeps its address on a DHCP server that files leases by MAC
  • #1419, the IPv4 client renews at RENEWING behind a DHCP relay, with the dhcp-golib v1.5.2 fix

The release line

flowchart LR
    v20["v2.0<br/>the project's own<br/>DHCP engine"]
    v21["v2.1<br/>the leased address in<br/>Docker's IPAM"]
    v22["v2.2<br/>IPv6 modes and<br/>router discovery"]
    v23["v2.3<br/>host plumbing"]
    v24["v2.4<br/>the rest of IPv6"]
    v25["v2.5<br/>CI, code debt and<br/>prefix delegation"]
    v26["v2.6<br/>IPv6-only networks and<br/>the IPAM IPv6 pool"]
    v27["v2.7<br/>per-container client_id"]
    v20 --> v21 --> v22 --> v23 --> v24 --> v25 --> v26 --> v27

v2.0 to v2.7 are released. There are no dates. Every release, patches included, is in the release notes.

The bar every feature is measured against

One docker network create line, then plain networks: [lan-dhcp] in any Compose file. No static IPs, no sidecars, no per-container plumbing, no entrypoint script that has to know it is running on this network.

That bar decides most design arguments before they start. A workaround a user has to script around the plugin is read here as a bug report against this principle. #125 is the worked example: containers landed on eth0, eth1 and so on in attach order, users scripted around the name, and the plugin now returns the requested interface name to the engine.

Where the project is today

This branch is the 2.x line. The plugin leases through the project's own in-tree DHCP client library, for IPv4 and IPv6, over bridge, macvlan and ipvlan, and serves Docker's IPAM contract as well as its network driver contract.

The driver reference is the authority on what exists and what each option does, and the release notes are the record of what each tag changed. Where this page disagrees with either of them, this page is wrong.

Direction

Themes, in rough order of how much they change for a user. The issue numbers are anchors; the order is not a queue.

Theme State Anchors
Quiet addressing faults RFC 5227 conflict detection runs inside the container's namespace for the life of the lease; every new failure mode gets a counter as well as a log line #524
Lease identity across recreates the DHCPv6 half shipped with 2.0; the deterministic MAC is blocked upstream and the ipvlan client-id carries no milestone #218, #219, #895
arm64 a native arm64 runner executes the full integration suite as a release-candidate gate, and the shipping shape is per-architecture tags #507, #531
A test substrate that cannot lie a test that only passes once something is weakened is treated as a bug report, and a gate enforces it #403
Supply chain and documentation releases are signed, provenanced, SBOM'd and reproducible; the remaining pull is the OpenSSF silver and gold criteria #452
The reasoning behind each theme

Quiet addressing faults. The hard failures were solved first; the quiet ones are the current work. v1.6.0 added conflict detection (#524) because the plugin accepted an address a statically-configured host already held and every counter stayed at zero. The container came up, Docker reported an address, and nothing anywhere said otherwise. 2.0 moved the check off the parent link and into the DHCP client, as RFC 5227 Address Conflict Detection running inside the container's own namespace for the whole life of the lease, with conflict_check choosing who pays for it. A counter that can read clean while the feature is broken is treated as an unfinished instrument.

Lease identity across recreates. A DHCP server keys on identity, so address stability is an identity problem. #218, the deterministic MAC, is backlog and blocked upstream. #219, a stable client-id for ipvlan where every child shares the parent's MAC, carries no milestone. 2.0 settled the DHCPv6 half: an ipvlan endpoint now gets a DUID of its own (#895).

arm64. Users asked for it and it works, proven on real hardware (#531). Per-architecture tags (vX.Y.Z-arm64, latest-arm64) were first published with v1.7.0 (#507). This page long held that a Docker plugin cannot be installed from a multi-architecture manifest list at all, so the architecture lives in the tag. A third-party registry serves one, and #1035 measured it: docker plugin install refuses a manifest list, and an OCI image index, on every engine from 20.10 to 29.8, by tag and by digest, while the per-architecture tags install.

A test substrate that cannot lie. This is infrastructure work with a user-visible reason: on this project, every timing crutch removed from CI turned out to be hiding a real defect. An opt-out helper added to make a restart test pass hid a user-facing docker restart failure for months. The question a loaded host raised, whether a slow Join leaves a container without a renewal client, was answered in 2.2 by making the attach path readable: every attach that completes is timed, and those durations with the namespace gauges say how close a host runs to its budget (#403).

Supply chain and documentation. The OpenSSF criteria are tracked as ordinary issues and generally translate into something concrete. This page exists because documentation_roadmap is one of them (#452).

Blocked upstream

Two features cannot ship until Docker's own engine carries a change. They stay open on purpose:

Here Needs Upstream
#218, the deterministic MAC network drivers to receive the endpoint name at CreateEndpoint, as IPAM drivers already do moby/moby#52870 (issue), moby/moby#52871 (PR, open)
#949, ipvlan with this plugin as the IPAM driver Docker to leave the container's interface MAC alone at start when the link cannot change it. Today it sets the MAC it generated for the IPAM driver, and every ipvlan container fails to start not filed yet

For #218, both halves were filed in June 2026. The endpoint-name change (moby/moby#52871) is still awaiting review, and #218 will not be closed as "won't fix" while that is the only thing in the way. This plugin's own half is written and waiting.

The second upstream dependency has moved. The interface_name pass-through (moby/moby#52866) merged and shipped in moby engine 29.8.0, and #125 closed with it. Engines below that boundary ignore a remote driver's requested interface name, the integration lane runs an engine above it, and the plugin counts ifname_unsupported below it.

Out of scope

These are decided, and the reasoning is recorded so a contributor can read it before writing the PR.

Decision Reason Anchor
It will not become a DHCP server your existing server is the authority; a second one would recreate the problem the plugin solves #111
It will not change interfaces the host already has the plugin reads host configuration and does not own it; the links it creates for its own networks are listed below #902, #903
It will not gain a static-IP workflow an address that must be fixed is fixed where addresses are decided, in a reservation on the DHCP server n/a
It will not ask for more privileges to buy a feature a capability added to config.json forces every operator to re-approve the plugin on upgrade #725
It will not detect a conflicting container on the same host RFC 5227 runs on the container's own link, and macvlan parent and child isolation hides a sibling that has taken our address #528
It refuses ipvlan L3 and L3S, see reference an L3 child sends no broadcast, so DHCP cannot start n/a
It will not run its arm64 verification under qemu-user or binfmt measured: the emulated plugin could not acquire a lease at all, and arm64 verification runs on real hardware #531
It will not backport security fixes only the latest release is supported, and upgrading is one docker plugin install SECURITY.md
It will not carry AI-assistant attribution in its history commits and PRs are signed by a person who stands behind them, and a CI check enforces it n/a
It will not ask for IPv6-Only Preferred (option 108) an 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 #1027
The reasoning behind the refusals

A second DHCP server. No lease serving, no built-in pool, no failover of its own. Interoperating with more than one server is a different question, and v1.8.0 answered it: dhcp_servers and dhcp_deny_servers decide which existing server a network leases from (#111). That stays on this side of the line: the plugin picks among authorities and never becomes one.

Host interfaces. The plugin will not bring a NIC up, add an address, or edit netplan or systemd-networkd. Two host links are inside this rule, because the plugin creates them for its own networks and removes them with the last one: since v2.3.0 a bridge made from a spare NIC that carries no address, when a bridge-mode network sets parent (#903), and a VLAN sub-interface of the parent (#902).

Static IPs. A per-container static-IP option would be a second, silently conflicting IPAM.

Privileges. Adding a capability to config.json forces every operator to re-approve the plugin's privileges on upgrade, and refusing that trade is the standing answer.

2.0 is what this rule costs when it is met head-on. The 2.0 line does add CAP_NET_RAW to config.json, and every operator upgrading onto it re-approves. It buys no feature: the DHCP exchange runs on an interface with no address, which requires an AF_PACKET socket on the ordinary path for every endpoint, with no configuration in which the plugin works without it. The power is unchanged, because the capability is in the OCI default set and the process always held it, so what the line bought is an honest manifest and what it cost is the prompt. Read the rule as written: never add one to buy a feature, and pay the re-approval out loud when the plugin genuinely needs the grant it is already exercising. See #725, whose title says the capability is already granted, which is true of the effective set the process runs with.

A conflicting container on the same host. macvlan's parent and child isolation, the same property that keeps a sibling from disturbing us, also hides a sibling that has taken our address. That is excluded by construction and is not pending work (#528).

Emulated arm64. On the 1.x client, which opened a NETLINK_GENERIC socket qemu-user does not translate, the emulated plugin could not acquire a lease at all. The 2.0 client is a different program and has not been re-measured under emulation; the conclusion is unchanged either way, because arm64 verification runs on real hardware and there is nothing to be gained by finding out which syscall the emulator drops next.

IPv6-Only Preferred. An endpoint on a network with an IPv4 pool must hold an IPv4 address: Docker's IPAM path refuses an answer with none, and the driver's own endpoint creation fails without one. That makes every DHCPv4 client of the plugin an IPv4-requiring host, and RFC 8925 section 3.2 says such a host must not put option 108 in its parameter request list. The plugin sets nothing and has no option for it; the DHCP library implements the option for consumers whose endpoints can live on IPv6 alone (#1027).

How this page is kept honest

The release runbook makes a top-to-bottom documentation review a release step, and this page is part of it. The milestone table and the lists under it are re-read against the tracker there. If a theme above has gone a year without motion, or a refusal has quietly become something the project does, that review is where it gets corrected.