Build a Docker Plugin with docker plugin create
docker plugin create turns a plain directory into a local, disabled Docker plugin, ready for Docker to inspect before you ever let it run. The directory must contain a plugin configuration file named config.json and a directory named rootfs. Allow about 20 minutes for a small test plugin, plus time to build the plugin program itself.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide covers Docker Community Edition CLI 29.8.1, packaged here as docker-ce-cli 5:29.8.1-1~ubuntu.24.04~noble. The command's interface is small, but the files it consumes describe a privileged extension to the Docker daemon. Treat the configuration and root filesystem as release artefacts, not an ad-hoc directory copied from an untrusted source.
1. Confirm the installed command
Run this as your ordinary user:
$ docker --version
Docker version 29.8.1, build 4a63305
$ docker plugin create --help
Usage: docker plugin create [OPTIONS] PLUGIN PLUGIN-DATA-DIR
Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory.
The only option is --compress, which compresses the context sent while creating the plugin. It does not touch your source directory and is not a switch for enabling the resulting plugin.
Checkpoint
The help output shows the two positional arguments in this order: the plugin name, then the data directory. A common mix-up is reading PLUGIN-DATA-DIR as the plugin's name; it is actually the directory containing the input files.
2. Prepare a separate plugin data directory
Choose a new working directory. This example creates only a directory layout and a harmless placeholder executable; swap it for your real plugin binary before creating a plugin you actually intend to use.
$ mkdir -p "$HOME/plugin-build/example/rootfs/usr/local/bin"
$ printf '%s\n' '#!/bin/sh' 'exec /usr/local/bin/example-plugin' > \
"$HOME/plugin-build/example/rootfs/usr/local/bin/example-plugin"
$ chmod 0755 "$HOME/plugin-build/example/rootfs/usr/local/bin/example-plugin"
$ find "$HOME/plugin-build/example" -maxdepth 5 -print
/home/your-user/plugin-build/example
/home/your-user/plugin-build/example/rootfs
/home/your-user/plugin-build/example/rootfs/usr
/home/your-user/plugin-build/example/rootfs/usr/local
/home/your-user/plugin-build/example/rootfs/usr/local/bin
/home/your-user/plugin-build/example/rootfs/usr/local/bin/example-plugin
Warning
Everything under rootfs becomes part of the plugin context. Do not put secrets, SSH keys, host configuration or an unnecessary device node in this tree. Keep the source outside a live service directory, so fixing the files cannot disrupt anything running.
3. Write config.json to match the program
Use the configuration format the plugin interface expects. This small volume-plugin-shaped example declares an entrypoint, a socket and a description. The executable named in Entrypoint must actually exist inside rootfs; the configuration file does not create it for you.
{
"Description": "Example volume plugin",
"Documentation": "https://example.invalid/example-plugin",
"Entrypoint": [
"/usr/local/bin/example-plugin"
],
"Interface": {
"Socket": "plugin.sock",
"Types": [
"docker.volumedriver/1.0"
]
},
"Linux": {
"Capabilities": null,
"AllowAllDevices": false,
"Devices": null
},
"Mounts": null,
"Network": {
"Type": "none"
},
"Workdir": ""
}
Change the interface type, entrypoint and permissions to match your own plugin. Docker's configuration reference also defines fields for mounts, environment variables, arguments, capabilities, devices, host namespaces and propagated mounts. These are requests for access, not harmless metadata: add only what the plugin genuinely needs, and have the plugin author review every capability, device and host-namespace setting.
Check the JSON before sending it to Docker:
$ python3 -m json.tool "$HOME/plugin-build/example/config.json" > /dev/null
$ test -d "$HOME/plugin-build/example/rootfs"
$ test -x "$HOME/plugin-build/example/rootfs/usr/local/bin/example-plugin"
true
That only proves the configured path exists and is executable. Syntactically valid JSON is not a working plugin: the entrypoint program must also implement the declared interface, and the entrypoint, interface and rootfs contents must all agree with each other.
4. Inspect the complete input before creation
Review names, permissions and file types without changing anything:
$ find "$HOME/plugin-build/example" -xdev -printf '%M %u:%g %p\n' | sort
$ stat "$HOME/plugin-build/example/config.json" \
"$HOME/plugin-build/example/rootfs"
$ sed -n '1,220p' "$HOME/plugin-build/example/config.json"
Check that the data directory contains exactly the intended config.json and rootfs at its top level. Do not point PLUGIN-DATA-DIR at something broad such as your home directory; that makes an accidentally included file far easier to miss.
5. Create the disabled local plugin
Creating a plugin changes Docker daemon state: it does not start the plugin, but it registers a new local plugin name. Check that the daemon is the intended Docker host and that the name is not already taken. This normally needs access to the Docker socket; use sudo only when your installation requires it.
$ docker plugin create example/plugin:trial \
"$HOME/plugin-build/example"
example/plugin:trial
With --compress, the same operation is:
$ docker plugin create --compress example/plugin:trial \
"$HOME/plugin-build/example"
example/plugin:trial
Do not run both commands for the same name. On success, Docker prints the plugin name, and it stays disabled until you explicitly enable it.
Checkpoint
If Docker reports lstat ...: no such file or directory, the data-directory path is wrong or has vanished. If it reports a missing config.json or rootfs, fix the source directory and retry. A failed create is never a reason to rerun the command with a wider path.
6. Verify the result before enabling it
List the plugin and inspect its configuration:
$ docker plugin ls
ID NAME DESCRIPTION ENABLED
... example/plugin:trial Example volume plugin false
$ docker plugin inspect example/plugin:trial
The exact ID and table spacing vary. What matters is ENABLED false. Inspect the capabilities, devices, mounts, network type, entrypoint and environment, not just the description. If a field is broader than the plugin needs, remove it from config.json, clean up the incorrect local plugin, then create a new test name.
Tip
Enabling is a separate, security-sensitive step outside this creation flow. A plugin may request network access, host namespaces, Linux capabilities, devices or a writable mount, so review those requests with the same care as a service running with elevated host access.
7. Recover from a bad local creation
Do not delete a plugin blindly if another workload might use it. Check its state and references first. Once you have confirmed the test plugin is unused and disabled, remove the exact name you created:
$ docker plugin disable example/plugin:trial
$ docker plugin rm example/plugin:trial
Recovery
If it was already disabled, the first command may simply report that no action is needed. If the plugin is enabled or in use, stop the dependent workload through its normal change process rather than forcing removal. The source directory stays on disk, so you can fix config.json or rootfs and create a new local tag without rebuilding the original inputs.
Done means
- CLI confirmed. The installed Docker CLI version and command syntax were checked.
- Directory is clean. The data directory contains exactly the intended
config.jsonandrootfs, with no accidental secrets or host files. - Entrypoint verified. The entrypoint in the configuration exists in the root filesystem with the required executable permission.
- Checked before creation. JSON syntax and the complete input tree were reviewed before creating the plugin.
- Confirmed disabled.
docker plugin lsshows the new plugin as disabled. - Permissions reviewed. Capabilities, devices, mounts and namespace access were checked before any enable operation.