Docker Daemon Panic: Nil Map Assignment With PublishAllPorts
Experiencing a Docker daemon panic can be frustrating, especially when it disrupts your workflow. This article delves into a specific panic scenario: "assignment to entry in nil map" in daemon/network.go when starting a container with PublishAllPorts=true. We'll break down the issue, discuss the cause, provide a way to reproduce it, and explore potential solutions.
Understanding the Issue
The core problem lies in how the Docker daemon (dockerd) handles port bindings when the PublishAllPorts option is enabled. Specifically, the panic occurs when a container is configured to publish all ports (-P or PublishAllPorts: true in the API) and the PortBindings field is passed as null in the JSON payload during container creation. This scenario highlights a potential regression in Docker's handling of nil maps during the container start sequence.
To clarify, a nil map in Go (the language Docker is written in) is a map that hasn't been initialized. Attempting to assign a value to a key in a nil map will result in a panic. In this case, the Docker daemon expects a valid (potentially empty) map for PortBindings when PublishAllPorts is true. When it receives null instead, it tries to assign port bindings to a non-existent map, leading to the dreaded panic.
The impact of this panic is significant. The API connection is severed, and the dockerd process crashes and restarts, disrupting any running containers and potentially leading to data loss or service downtime. While using the Docker CLI might mask this issue (as it likely initializes the map as empty {} rather than null), the problem surfaces when using the Docker API directly, particularly through tools like the Rust testcontainers library, which can send null for PortBindings.
Reproducing the Panic
To fully grasp the issue, reproducing it in a controlled environment is beneficial. The following bash script demonstrates how to trigger the panic using curl to interact with the Docker API directly:
#!/bin/bash
docker pull redis:latest > /dev/null 2>&1
# 1. Create Container (PortBindings: null)
ID=$(curl -s --unix-socket /var/run/docker.sock \
-H "Content-Type: application/json" \
-X POST http://localhost/v1.45/containers/create \
-d '{
"Image": "redis:latest",
"ExposedPorts": { "6379/tcp": {} },
"HostConfig": { "PublishAllPorts": true, "PortBindings": null }
}' | jq -r .Id)
echo "Created Container: $ID"
curl -v --unix-socket /var/run/docker.sock \
-X POST "http://localhost/v1.45/containers/$ID/start"
This script first pulls the redis:latest image. Then, it uses curl to send a POST request to the Docker API's /containers/create endpoint. The key part is the JSON payload, which specifies:
Image: Theredis:latestimage to use.ExposedPorts: Declares that port6379/tcpis exposed.HostConfig: This is where the problem lies:PublishAllPorts: Set totrue, instructing Docker to publish all exposed ports to random host ports.PortBindings: Set tonull, which is the trigger for the panic.
The script then retrieves the container ID and attempts to start the container using the /containers/{id}/start endpoint.
After running this script, you can check the Docker daemon logs using journalctl -u docker -n 50. You should see the panic traceback, confirming the issue. The relevant log snippet will look similar to this:
Nov 29 02:19:09 fedora dockerd[60398]: 2025/11/29 02:19:09 http: panic serving @: assignment to entry in nil map
Nov 29 02:19:09 fedora dockerd[60398]: goroutine 884 [running]:
Nov 29 02:19:09 fedora dockerd[60398]: net/http.(*conn).serve.func1()
Nov 29 02:19:09 fedora dockerd[60398]: /usr/local/go/src/net/http/server.go:1943 +0xd3
Nov 29 02:19:09 fedora dockerd[60398]: panic({0x55e6e768bd00?, 0x55e6e92afda0?})
Nov 29 02:19:09 fedora dockerd[60398]: /usr/local/go/src/runtime/panic.go:783 +0x132
Nov 29 02:19:09 fedora dockerd[60398]: [github.com/moby/moby/v2/daemon.buildPortsRelatedCreateEndpointOptions(0xc000ad6008](https://github.com/moby/moby/v2/daemon.buildPortsRelatedCreateEndpointOptions(0xc000ad6008), 0x55e6e7b90120?, 0xc000690780)
Nov 29 02:19:09 fedora dockerd[60398]: /root/rpmbuild/BUILD/docker-ce-29.1.1-build/src/engine/daemon/network.go:1047 +0xae6
Nov 29 02:19:09 fedora dockerd[60398]: [github.com/moby/moby/v2/daemon.buildCreateEndpointOptions(0xc000ad6008](https://github.com/moby/moby/v2/daemon.buildCreateEndpointOptions(0xc000ad6008), 0xc00079f1e0, 0xc0013b78f0, 0xc000690780, {0x0, 0x0, 0x0?})
Nov 29 02:19:09 fedora dockerd[60398]: /root/rpmbuild/BUILD/docker-ce-29.1.1-build/src/engine/daemon/network.go:988 +0x2bf
Nov 29 02:19:09 fedora dockerd[60398]: [github.com/moby/moby/v2/daemon.(*Daemon).connectToNetwork(0xc0007d0a08](https://github.com/moby/moby/v2/daemon.(*Daemon).connectToNetwork(0xc0007d0a08), {0x55e6e7bf0538, 0xc0011706f0}, 0xc0007e0008, 0xc000ad6008, {0x55e6e6e694cd, 0x6}, 0xc0013b78f0)
Nov 29 02:19:09 fedora dockerd[60398]: /root/rpmbuild/BUILD/docker-ce-29.1.1-build/src/engine/daemon/container_operations.go:738 +0x96e
The key line to look for is panic serving @: assignment to entry in nil map, which clearly indicates the nature of the problem.
Expected Behavior
The Docker daemon should gracefully handle the scenario where PortBindings is null when PublishAllPorts is true. Ideally, it should treat null as an empty map, effectively meaning no specific port bindings are defined, but all exposed ports should still be published to random host ports as dictated by PublishAllPorts. This behavior is crucial for maintaining stability and preventing unexpected crashes.
Before Docker version 29.x, this panic did not occur, suggesting a regression in the handling of nil maps within the daemon's networking logic.
Analyzing the Root Cause
The panic trace points to the daemon/network.go file within the Docker codebase, specifically the buildPortsRelatedCreateEndpointOptions function. This function is responsible for constructing the endpoint options related to port publishing and mapping. The issue likely stems from a conditional check or initialization logic within this function that fails to properly handle the PortBindings field when it's null.
Without access to the exact code implementation, it's challenging to pinpoint the precise line causing the panic. However, based on the traceback, the following scenario seems plausible:
- The
buildPortsRelatedCreateEndpointOptionsfunction receives anullvalue forPortBindings. - It attempts to iterate or assign values to the
PortBindingsmap without first checking if it's nil. - This leads to an attempt to write to an uninitialized map, triggering the panic.
The fix would likely involve adding a check for nil before attempting to access or modify the PortBindings map. If it's nil, the function should either initialize an empty map or handle the case appropriately without causing a panic.
Solutions and Workarounds
While a permanent fix requires a code change within the Docker daemon itself, several workarounds can be employed to avoid the panic in the meantime:
-
Avoid Passing
nullforPortBindings: The simplest solution is to ensure that you never sendnullas the value forPortBindingsin the API payload. Instead, pass an empty JSON object ({}) to represent an empty map. This explicitly tells Docker that there are no specific port bindings, but it avoids the nil map issue.Modify the script above like this: