22 KiB
Docker CLI design guidelines
This document provides guidelines to develop new features and enhancements for the Docker CLI.
This is not an exhaustive set of rules, so in case of doubt, it may be good to discuss with a maintainer. This document is meant to be a living document; if you see something out of date or missing, pull requests are welcome.
This document intends to;
- Provide a consistent, predictable UX for users of the Docker CLI
- Assist contributors and maintainers when reviewing changes, providing them guidelines to verify the design.
General acceptance criteria
Problem description
Features should address a problem or use-case. When contributing a new feature, describe the use-case or problem that it is addressing. Having actual examples not only helps to verify if the design matches the expectations, but can also assist other participants to verify if the proposed solution is the only (or "best") solution.
Docker Compose file
Any feature added should usually also be added to the docker-compose schema;
- Propose new additions to the compose spec in the Compose Spec repository.
- Update schema files in this repository
Documentation
Any feature added should be accompanied by at least:
- A mention in the corresponding reference documentation.
- An example in the reference documentation; the example should make clear why a feature should be used (clear use-case)
For larger features, additional documentation may be needed in the main documentation repository. The documentation team can help with writing that documentation, but technical assistance from contributors is generally appreciated.
Completion scripts
New commands and flags should be added to the completion scripts. Help can be provided in updating those scripts; it's acceptable to update completion scripts in a follow-up pull requests, but a tracking issue must be created in that case.
- Bash completion (required)
- PowerShell (optional)
- Fish (optional)
- Zsh (optional)
Technical / design debt
The Docker CLI evolved over time, which also means that the design inherited some design-choices from the past that may not have been the best choices (in hindsight), but cannot be changed without introducing breaking changes.
This section describes some of these behaviors.
Legacy top level commands
Historically, the Docker CLI had a limited set of commands, to manage images
and containers (docker run
, docker pull
, docker push
). With the introduction
of other type of objects (volumes, networks, services, plugins), this pattern
did not scale.
No new top-level commands should be added; top-level commands are reserved for management commands going forward.
Note: some less-frequently used legacy top-level commands can be hidden by setting the
DOCKER_HIDE_LEGACY_COMMANDS
environment variable. Setting this variable hides the commands, but they will still remain active for backward compatibility.
Exit code for filtered results
When filtering results, and no results were found, the CLI produces a zero (success) exit code. Reason for this is that the action was successful, but happened to not produce any matching results.
For example;
docker container ls --filter status=exited
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
echo $?
0
ls *.bla 2> /dev/null || echo "no such thing"
no such thing
Producing a zero exit code can complicate using these commands in scripting situations, but has been considered too much of a breaking change to change (See #27657 and #28951).
Single-value flags can be specified multiple times
The Docker CLI accepts flags to be set multiple times, even if an option that's set through that flag accepts a single value. If a single-value flag is set multiple times, no error is produced, and latter values override prior values.
For example, the following command runs successfully;
container create --name one --name two --name three busybox
c872b39344646e86ef7d83f846e47f0cb92abc200938187f87052737026433d1
echo $?
0
And creates a container named three
:
docker container inspect --format '{{.Name}}' c872b39344646e86ef7d83f846e47f0cb92abc200938187f87052737026433d1
/three
Note: it may be worth revisiting this situation, and only keep this behavior for existing flags (for backward compatibility), and enforce single- value flags to be specified only once going forward.
Passing input from stdin
There is some inconsistency in notations used to pass input from stdin
.
Some commands accept a path as positional argument, and -
to accept input from
stdin
:
Usage: docker build [OPTIONS] PATH | URL | -
Usage: docker config create [OPTIONS] CONFIG file|-
Usage: docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
Whereas (e.g.) docker load
default to using stdin/stdout
, and require -i
/
--input
/ -o
/ --output
to be passed to use a file instead.
Usage: docker save [OPTIONS] IMAGE [IMAGE...]
Usage: docker load [OPTIONS]
Usage: docker export [OPTIONS] CONTAINER
Which allows passing
echo $SOME_ENV_VAR | docker config create myconfig -
printf 'my configuration' | docker config create myconfig -
docker config create myconfig - <<< "my configuration"
docker config create myconfig - <<-'EOF'
line 1
line 2
EOF
Sending output to stdout
Todo: we need to describe the canonical approach going forward.
General guidelines
Keep it simple
Before contributing a feature, consider if the feature can be addressed through
other means. Try to adhere to the Linux principle;
"do one thing, and do it well". If a use-case can be addressed combining
commands (e.g. docker container ls -q | xargs docker container inspect
).
Do not prematurely optimize usability
Do not use shorthand (single-letter) flags
Shorthand, single-letter flags can easily become ambiguous (for example, -f
can be either a shorthand for --format
or for --force
).
For this reason, shorthand flags must be reserved for frequently used options only, and only if there is a need. As with all changes, it is easier to add a shorthand option later, than to remove an option once added.
Standard flags/options are an exception to this rule, for example, list-commands
that have a --format
option should generally also get a -f
shorthand.
Avoid microformats
Configuration, and order of preference
- Flag
- Environment variable
- Configuration
- not everything should be configurable. no direct need? don't do it
Linux principle, and "chainable"
Naming conventions
- Avoid product names: prefer "generic"
- Describe what it does, not how the product is named
API and feature compatibility
The Docker CLI should be compatible with older versions of the API. If a feature depends on a specific API version, or (for example) requires an orchestrator to be enabled, then the feature should be hidden if those conditions are not met.
For example, the following code defines a --foo
flag that requires API version
1.99
. The flag will be hidden if the Docker CLI connects with a daemon that
does not support this version of the API.
flags.BoolVar(&options.foo, "foo", false, "Foo enables foo on a container")
flags.SetAnnotation("foo", "version", []string{"1.99"})
The example below shows a foo
command that requires a daemon with API version
1.99 and experimental features enabled. In this example, foo
is a top-level
command, and all sub-commands will have the same requirements.
// NewFooCommand returns a cobra command for `foo` subcommands
func NewFooCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "foo",
Short: "Manage foos",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{
"version": "1.99",
"experimental": "",
},
}
cmd.AddCommand(
newBarCommand(dockerCli),
newBazCommand(dockerCli),
)
return cmd
}
Annotations exist for orchestrators (kubernetes, swarm), experimental features,
and builder version. Some annotations do not require a value, in which case nil
should be used.
Annotation | Example value | Description |
---|---|---|
version |
1.40 |
Feature requires API version 1.40 , and is hidden on any older API version. |
experimental |
nil |
Feature requires a daemon with experimental features enabled, and is hidden otherwise. |
no-buildkit |
nil |
Feature is only used when using the legacy builder, and is hidden if BuildKit is used as builder. |
orchestrator |
nil |
Feature requires an orchestrator (SwarmKit or Kubernetes) and is hidden otherwise. |
kubernetes |
nil |
Feature requires the Kubernetes orchestrator, and is hidden otherwise. |
swarm |
nil |
Feature requires the SwarmKit orchestrator, and is hidden otherwise. |
Validation
Docker uses a client/server architecture. As a consequence, the environment in which the Docker CLI runs may not match the environment of the daemon. For this reason, validation on the client-side should be limited, and deferred as much as possible to the daemon.
Offloading validation to the daemon prevents validation-rules from diverging between both, and prevents the similar code having to be maintained twice. Or (worse) having validation code in the client, but unhandled by the daemon.
In addition; do not expect what's valid to never change (what's invalid today, may be supported by the daemon, or the kernel, tomorrow).
As a rule of thumb: it's the CLI's responsibility to convert commands and arguments into API requests. If the CLI is able to handle a value, and turn it into an a valid API request (even if values in the request are invalid), that responsibility is met. If the request fails, it should handle the error, and (where needed) present it to the user.
Some examples of validations:
OK | Validation | Description |
---|---|---|
✅ | Check if a required command-line argument is set | If a command requires an argument, it is ok to check if the argument is provided. |
✅ | Validate if a numeric value only contains numbers | This is ok. If the API requires a numeric value, the CLI should be able to convert the user-provided value. |
❌ | Check if a name only contains allowed characters | Rules for (e.g.) names can change over time, thus differ between daemon versions. This validation should be done by the daemon, and returned as an error. |
✅ | Validate if a file exists before uploading it | This is ok. The client must be able to access the file (or directory) in order to upload it. |
❌ | Check if the host-path of a bind-mount exists | Bind-mounts are done on the host where the daemon runs, therefore validation of these paths should not be done by the CLI. |
🔶 | Validate if an URL option is well-formed | Generally this should be ok if the API expects an URL value. Consider if the validation adds value; if the CLI is able to create an API request, even if the value is invalid, validation could be offloaded to the daemon. |
Sanitizing and normalizing user input
Avoid string manipulation, unless necessary. Prefer strictness of user-provided values over "fuzzy" matching / "guessing" user intent. Producing an error, and loosen validation over time can be done without breaking backward compatibility (it's guaranteed that no user was using the invalid value), whereas becoming more strict requires a deprecation cycle ("xx is no longer valid, and support will be removed in release XX.YY").
OK | Validation | Description |
---|---|---|
✅ | Trim leading and trailing whitespace | Generally ok |
🔶 | Sort values | Tread carefully. Sorting may be required to prevent Swarm services from being updated if no changes were made, but in some cases "order matters". |
❌ | Strip quotes | Should be handled by the shell |
❌ | Cast strings to lowercase / uppercase | Avoid string value manipulation if not needed. |
Commands and subcommands
Standardized "CRUD" commands and aliases
create
remove
,rm
update
list
,ls
inspect
(JSON)
Command | Aliases | Description |
---|---|---|
docker <object> create |
- | Creates a new <object> |
docker <object> list |
ls / ps |
Presents a list of <object> (table view by default) |
docker <object> inspect <id> |
- | Provides low-level information about <object> <id> in JSON format |
docker <object> update <id> |
- | Updates <object> <id> |
docker <object> remove <id> |
rm |
|
docker <object> prune |
- |
Flags
Shorthand (single-letter) flags
Standardized flags
Flag | Generally used on | Description |
---|---|---|
--format / -f |
list , inspect |
Pretty-print objects using a Go template |
--filter / -f |
list |
Filter objects based on conditions provided |
--no-trunc |
list |
List outputs can truncate columns to save screen-space. The --no-trunc option prints columns without truncating |
--quiet / -q |
list |
For list outputs; only print object ID's. |
File flags
- absolute vs relative paths
- support for stdin (CONVENTION??)
Feedback on successful operations
Successful operations on an object should print the object's identifier on stdout
on success; doing so enables users to consume the output for scripting.
For example, the following command creates two volumes, using different drivers, and attaches those volumes to a new container:
docker container run \
--volume $(docker volume create --driver=foo):/somewhere \
--volume $(docker volume create --driver=bar):/somewhere-else \
busybox
Either ID
or name
are acceptable, as long as the identifier can be used
to reference the object.
Some examples of commands that follow this design:
Creating a container prints the container's ID
on stdout
docker container create --name test busybox
cc1555ede50a11f02a3ef6cc8eedd9f78f6299a2f2b7efdc8a91fbefb8fc194a
Removing a container prints the reference that was given
docker container rm test
test
docker container rm 057d35243903e522c70ae2dfab5f706e4ea3cc1ae4c38cce955a99be3d09cfd1
057d35243903e522c70ae2dfab5f706e4ea3cc1ae4c38cce955a99be3d09cfd1
Use of stdout and stderr
As a rule of thumb;
- Use
stdout
to print the expected output of a command - Where possible make
stdout
usable for scripting - Use
stderr
non-standard output (errors), and for informational messages.
Sometimes these differences are subtle.
Example: usage information.
The docker
command expects a subcommand. If no subcommand is given, the Docker
CLI prints an informational message, showing the usage information:
docker
Usage: docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
...
Typing docker --help
also prints the usage information:
docker --help
Usage: docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
...
At a glance, both appear to be identical, but there is a difference:
In the first example, stdout
contains the "expected" output of the docker
command; the docker
command requires a subcommand, and by itself does not
produce a result.
The "usage" output is informational, and therefore printed on stderr
, which
can be seen when discarding the stderr
output by redirecting it to /dev/null
;
docker 2> /dev/null
# no output (stdout contains no output)
When using the --help
flag, the usage information is the expected output,
and therefore printed on stdout
. Suppressing stderr
output shows that all
output is this time printed on stdout
:
docker --help 2> /dev/null
Usage: docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
Practical example: docker run
logging informational messages and consuming stdout output
docker container run -d nginx:alpine | xargs docker container inspect --format 'the name of the started container is: {{.Name}}'
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
cd784148e348: Already exists
6e3058b2db8a: Already exists
7ca4d29669c1: Already exists
a14cf6997716: Already exists
Digest: sha256:385fbcf0f04621981df6c6f1abd896101eb61a439746ee2921b26abc78f45571
Status: Downloaded newer image for nginx:alpine
the name of the started container is: /vigilant_zhukovsky
docker container run -d nginx:alpine 2> ./err.log | xargs docker container inspect --format '{{.Name}}'
/cranky_jepsen
cat err.log
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
cd784148e348: Already exists
6e3058b2db8a: Already exists
7ca4d29669c1: Already exists
a14cf6997716: Already exists
Digest: sha256:385fbcf0f04621981df6c6f1abd896101eb61a439746ee2921b26abc78f45571
Status: Downloaded newer image for nginx:alpine
docker service create nginx:alpine
trppp1sywrkegq8e4pynjenv0
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged
docker service create nginx:alpine 2> /dev/null
clu6qi8pcg2fbnhxpuusdk0iw
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged
docker service create --detach nginx:alpine
q5d2k2bbgk6uh2rs3g4udfdh7
Object identifiers
Objects can have multiple identifiers, for example, containers have both an
ID
and a name
, both of which must be unique (and are thus interchangeable).
Names are allowed to be mutable (for example, a container can be renamed using
the docker container rename
command).
Objects that have both an ID
and name
, and where commands accept either an
ID
, a name
or a partial ID
(ID-prefix) should address ambiguity by
prioritizing as below:
- Full, non-truncated
ID
name
(full match only, no prefix matching)- Partial
ID
(prefix matching)
If multiple objects match a given ID prefix, an error must be produced, stating that the given prefix is ambiguous.
For example, given the following containers:
docker ps --no-trunc --format 'table {{.ID}}\t{{.Names}}'
CONTAINER ID NAMES
70d50d097b5597d8d08171e6be51cee9e15083c5b92dde134639c4105db2a40d mycontainer01
724f22e1e32c7cc8d80383f06baebbcd0f5fec0e5592bdf6b50b2bcd8d391ab7 70d50d097b55
737ff55584e28badc5fdccd22582f6d4583d0f792cbb4924493f5d341e44a278 70d50d097b5597d8d08171e6be51cee9e15083c5b92dde134639c4105db2a40d
Note that:
- All containers have an
ID
starting with7
- The second container's name matches the first container's "short"
ID
(ID-prefix) - The third container's name matches the first container's full
ID
Running docker container inspect
using the first container's full ID produces
the first container (full ID
takes precedence over full name);
docker container inspect 70d50d097b5597d8d08171e6be51cee9e15083c5b92dde134639c4105db2a40d --format '{{.Name}}'
/mycontainer01
Inspecting using the second container's full name, produces that container,
even though it also matches a prefix of the first container's ID (full name match
takes precedence over a partial ID
match):
docker container inspect 70d50d097b55 --format '{{.Name}}'
/70d50d097b55
Inspecting using a prefix produces an error, because multiple objects are matched:
docker container inspect 7 --format '{{.Name}}'
Error response from daemon: Multiple IDs found with provided prefix: 7
Using a longer prefix will succeed if the prefix is non-ambiguous;
docker container inspect 70 --format '{{.Name}}'
/mycontainer01
Progressbars
- automatically disabled if no terminal is detected
- also through
--quiet
/-q
flag
Boolean flags should expect no value
exception flags that will change their default in the near future
--disable-some-feature=false
--enable-almost-deprecated-feature=false
Shorthand flag formats
- avoid microformats
- keep Windows/Linux into account
- strict > permissive (easier to be less restrictive in future than the other way round)
- case-sensitive values (
none
!=None
!=NONE
!=nOnE
)
- case-sensitive values (
Long form (advanced) syntax
- group all options for a configuration in a single flag
- allow that flag to be set multiple times, and still being able to group those
options together (example:
--volume-driver
,--volume
, which prevented multiple drivers to be used)
Positional arguments
Positional arguments are generally more suitable for "required" arguments, whereas flags are for "optional" arguments.
Examples:
Creating a container does not require a name to be specified (a name is generated when omitted), hence, the name is passed as
docker container create busybox:latest
47931878b128c40281aa0254914c3a437b2fdf91133a1620b3152e05d3c81e5d
docker container create --name mycontainer busybox:latest
908a6f802740d9a67861fc031c62d6241fbb9f16b9388114b14cf120e4f0cd68
Use of stdout and stderr
- output should be useful
Exit codes
- exit code for list commands (exit code 0 for "no results")
Filtering
Formatting output
- Discuss
JSON
output option - Presentation should not be in
JSON
output (or the API for that matter) - Configurable in
~/.docker/config.json