DockerCLI/DESIGN_GUIDELINES.md

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;

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

  1. Flag
  2. Environment variable
  3. 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:

  1. Full, non-truncated ID
  2. name (full match only, no prefix matching)
  3. 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 with 7
  • 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)

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