diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go
index c0370f95e5f2..91d4945ed9ef 100644
--- a/cli/command/container/stats.go
+++ b/cli/command/container/stats.go
@@ -37,6 +37,11 @@ type StatsOptions struct {
// output such as container-IDs.
NoTrunc bool
+ // Sort is the column to sort the output by. Accepted values are "cpu", "mem",
+ // "mem%", "net", "block", and "pids" (descending), and "name" and "id" (ascending). If empty,
+ // containers are printed in the order they were added.
+ Sort string
+
// Format is a custom template to use for presenting the stats.
// Refer to [flagsHelper.FormatHelp] for accepted formats.
Format string
@@ -88,6 +93,8 @@ func newStatsCommand(dockerCLI command.Cli) *cobra.Command {
flags.BoolVar(&options.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&options.NoTrunc, "no-trunc", false, "Do not truncate output")
flags.StringVar(&options.Format, "format", "", flagsHelper.FormatHelp)
+ flags.StringVar(&options.Sort, "sort", "", `Sort output by column ("cpu", "mem", "mem%", "net", "block", "name", "id", "pids")`)
+ _ = cmd.RegisterFlagCompletionFunc("sort", completion.FromList("cpu", "mem", "mem%", "net", "block", "name", "id", "pids"))
return cmd
}
@@ -107,6 +114,11 @@ var acceptedStatsFilters = map[string]bool{
//
//nolint:gocyclo
func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
+ options.Sort = normalizeStatsSortKey(options.Sort)
+ if _, ok := statsSortKeys[options.Sort]; options.Sort != "" && !ok {
+ return errdefs.ErrInvalidArgument.WithMessage("invalid sort key '" + options.Sort + "': must be one of cpu, mem, mem%, net, block, name, id, pids")
+ }
+
apiClient := dockerCLI.Client()
// Get the daemonOSType to handle platform-specific stats fields.
@@ -307,6 +319,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
for _, c := range statsList {
ccStats = append(ccStats, c.GetStatistics())
}
+ sortStats(ccStats, options.Sort)
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
return err
}
@@ -331,6 +344,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
for _, c := range statsList {
ccStats = append(ccStats, c.GetStatistics())
}
+ sortStats(ccStats, options.Sort)
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
return err
diff --git a/cli/command/container/stats_helpers.go b/cli/command/container/stats_helpers.go
index 66571b530942..0d6c9fe6f390 100644
--- a/cli/command/container/stats_helpers.go
+++ b/cli/command/container/stats_helpers.go
@@ -1,10 +1,13 @@
package container
import (
+ "cmp"
"context"
"encoding/json"
"errors"
"io"
+ "slices"
+ "strings"
"sync"
"time"
@@ -271,3 +274,35 @@ func calculateMemPercentUnixNoCache(limit float64, usedNoCache float64) float64
}
return 0
}
+
+// statsSortKeys maps the values accepted by "docker stats --sort" to a
+// comparison function. Resource columns sort in descending order (highest
+// usage first), and "name" sorts in ascending order.
+var statsSortKeys = map[string]func(a, b StatsEntry) int{
+ "cpu": func(a, b StatsEntry) int { return cmp.Compare(b.CPUPercentage, a.CPUPercentage) },
+ "mem": func(a, b StatsEntry) int { return cmp.Compare(b.Memory, a.Memory) },
+ "mem%": func(a, b StatsEntry) int { return cmp.Compare(b.MemoryPercentage, a.MemoryPercentage) },
+ "net": func(a, b StatsEntry) int { return cmp.Compare(b.NetworkRx+b.NetworkTx, a.NetworkRx+a.NetworkTx) },
+ "block": func(a, b StatsEntry) int { return cmp.Compare(b.BlockRead+b.BlockWrite, a.BlockRead+a.BlockWrite) },
+ "pids": func(a, b StatsEntry) int { return cmp.Compare(b.PidsCurrent, a.PidsCurrent) },
+ "name": func(a, b StatsEntry) int { return strings.Compare(a.Name, b.Name) },
+ "id": func(a, b StatsEntry) int { return strings.Compare(a.ID, b.ID) },
+}
+
+func normalizeStatsSortKey(key string) string {
+ return strings.ToLower(key)
+}
+
+// sortStats sorts entries in place by the given key. Entries with equal
+// values are ordered by container so that output is stable between frames.
+// An empty key leaves the entries in their original order.
+func sortStats(entries []StatsEntry, key string) {
+ key = normalizeStatsSortKey(key)
+ compare, ok := statsSortKeys[key]
+ if !ok {
+ return
+ }
+ slices.SortFunc(entries, func(a, b StatsEntry) int {
+ return cmp.Or(compare(a, b), strings.Compare(a.Container, b.Container))
+ })
+}
diff --git a/cli/command/container/stats_helpers_test.go b/cli/command/container/stats_helpers_test.go
index 081b6be52012..49f0eaf95e20 100644
--- a/cli/command/container/stats_helpers_test.go
+++ b/cli/command/container/stats_helpers_test.go
@@ -61,3 +61,39 @@ func inDelta(x, y, delta float64) func() (bool, string) {
return true, ""
}
}
+
+func TestSortStats(t *testing.T) {
+ entries := func() []StatsEntry {
+ return []StatsEntry{
+ {Container: "a", ID: "3", Name: "web", CPUPercentage: 10, Memory: 300, MemoryPercentage: 30, NetworkRx: 10, NetworkTx: 30, BlockRead: 20, BlockWrite: 10, PidsCurrent: 2},
+ {Container: "b", ID: "2", Name: "db", CPUPercentage: 50, Memory: 100, MemoryPercentage: 10, NetworkRx: 50, NetworkTx: 10, BlockRead: 10, BlockWrite: 50, PidsCurrent: 9},
+ {Container: "c", ID: "1", Name: "cache", CPUPercentage: 10, Memory: 200, MemoryPercentage: 20, NetworkRx: 20, NetworkTx: 20, BlockRead: 30, BlockWrite: 20, PidsCurrent: 5},
+ }
+ }
+ containers := func(s []StatsEntry) []string {
+ out := make([]string, 0, len(s))
+ for _, e := range s {
+ out = append(out, e.Container)
+ }
+ return out
+ }
+
+ for key, expected := range map[string][]string{
+ "": {"a", "b", "c"}, // unsorted: original order
+ "cpu": {"b", "a", "c"}, // descending, tie broken by container
+ "mem": {"a", "c", "b"},
+ "mem%": {"a", "c", "b"},
+ "net": {"b", "a", "c"},
+ "block": {"b", "c", "a"},
+ "pids": {"b", "c", "a"},
+ "name": {"c", "b", "a"}, // ascending
+ "id": {"c", "b", "a"}, // ascending
+ "CPU": {"b", "a", "c"}, // case-insensitive
+ } {
+ t.Run(key, func(t *testing.T) {
+ s := entries()
+ sortStats(s, key)
+ assert.DeepEqual(t, containers(s), expected)
+ })
+ }
+}
diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker
index b9b7a28ec263..17d0c9685a49 100644
--- a/contrib/completion/bash/docker
+++ b/contrib/completion/bash/docker
@@ -2241,11 +2241,15 @@ _docker_container_stats() {
--format)
return
;;
+ --sort)
+ COMPREPLY=( $( compgen -W "cpu mem mem% net block name id pids" -- "$cur" ) )
+ return
+ ;;
esac
case "$cur" in
-*)
- COMPREPLY=( $( compgen -W "--all -a --format --help --no-stream --no-trunc" -- "$cur" ) )
+ COMPREPLY=( $( compgen -W "--all -a --format --help --no-stream --no-trunc --sort" -- "$cur" ) )
;;
*)
__docker_complete_containers_running
diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish
index bf1dd38b3b0c..9b9a7f45e08e 100644
--- a/contrib/completion/fish/docker.fish
+++ b/contrib/completion/fish/docker.fish
@@ -524,6 +524,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from start' -a '(__fish_prin
complete -c docker -f -n '__fish_docker_no_subcommand' -a stats -d "Display a live stream of one or more containers' resource usage statistics"
complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l help -d 'Print usage'
complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l no-stream -d 'Disable streaming stats and only pull the first result'
+complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l sort -a 'cpu mem mem% net block name id pids' -d 'Sort output by column'
complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -a '(__fish_print_docker_containers running)' -d "Container"
# stop
diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker
index 1efb30f86a8a..4a999ca6a7de 100644
--- a/contrib/completion/zsh/_docker
+++ b/contrib/completion/zsh/_docker
@@ -908,6 +908,7 @@ __docker_container_subcommand() {
"($help)--format=[Format the output using the given Go template]:template: " \
"($help)--no-stream[Disable streaming stats and only pull the first result]" \
"($help)--no-trunc[Do not truncate output]" \
+ "($help)--sort=[Sort output by column]:column:(cpu mem mem% net block name id pids)" \
"($help -)*:containers:__docker_complete_running_containers" && ret=0
;;
(stop)
diff --git a/docs/reference/commandline/container_stats.md b/docs/reference/commandline/container_stats.md
index c1a9a44e9134..5abf5760f039 100644
--- a/docs/reference/commandline/container_stats.md
+++ b/docs/reference/commandline/container_stats.md
@@ -15,6 +15,7 @@ Display a live stream of container(s) resource usage statistics
| [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `--no-stream` | `bool` | | Disable streaming stats and only pull the first result |
| `--no-trunc` | `bool` | | Do not truncate output |
+| [`--sort`](#sort) | `string` | | Sort output by column (`cpu`, `mem`, `mem%`, `net`, `block`, `name`, `id`, `pids`) |
@@ -129,6 +130,22 @@ CONTAINER ID NAME CPU % PRIV WORKING SET
9db7aa4d986d mad_wilson 9.59% 40.09 MiB 27.6 kB / 8.81 kB 17 MB / 20.1 MB
```
+### Sort the output (--sort)
+
+Use the `--sort` option to order containers by a column, similar to
+`kubectl top --sort-by`. Resource columns (`cpu`, `mem`, `mem%`, `net`, `block`,
+`pids`) sort in descending order so the busiest containers appear first; `name`
+and `id` sort in ascending order.
+
+```console
+$ docker stats --no-stream --sort mem
+
+CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
+67b2525d8ad1 db 0.31% 412.6MiB / 1.952GiB 20.64% 2.48kB / 0B 4.11MB / 0B 12
+b95a83497c91 web 0.28% 35.63MiB / 1.952GiB 1.78% 916B / 0B 147kB / 0B 9
+e5c383697914 cache 0.00% 5.196MiB / 1.952GiB 0.26% 71.2kB / 0B 770kB / 0B 1
+```
+
### Format the output (--format)
The formatting option (`--format`) pretty prints container output
diff --git a/docs/reference/commandline/stats.md b/docs/reference/commandline/stats.md
index efb8cd129155..e64b667b894e 100644
--- a/docs/reference/commandline/stats.md
+++ b/docs/reference/commandline/stats.md
@@ -15,6 +15,7 @@ Display a live stream of container(s) resource usage statistics
| `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `--no-stream` | `bool` | | Disable streaming stats and only pull the first result |
| `--no-trunc` | `bool` | | Do not truncate output |
+| `--sort` | `string` | | Sort output by column (`cpu`, `mem`, `mem%`, `net`, `block`, `name`, `id`, `pids`) |
diff --git a/man/src/container/stats.md b/man/src/container/stats.md
index f824417ff23f..d243061284ca 100644
--- a/man/src/container/stats.md
+++ b/man/src/container/stats.md
@@ -1,5 +1,10 @@
Display a live stream of one or more containers' resource usage statistics
+# SORT
+
+Use `--sort` to order output by `cpu`, `mem`, `mem%`, `net`, `block`, `pids`,
+`name`, or `id`. Resource values sort descending; names and IDs sort ascending.
+
# Format
Format the output using the given Go template.
@@ -45,4 +50,4 @@ Running `docker container stats` with customized format on all (Running and Stop
Running `docker container stats` in JSON format.
$ docker container stats --format json
- {"BlockIO":"43.9MB / 0B","CPUPerc":"0.00%","Container":"14c505d03da8","ID":"14c505d03da8","MemPerc":"0.21%","MemUsage":"4.242MiB / 1.944GiB","Name":"registry","NetIO":"4.17kB / 0B","PIDs":"13"}
\ No newline at end of file
+ {"BlockIO":"43.9MB / 0B","CPUPerc":"0.00%","Container":"14c505d03da8","ID":"14c505d03da8","MemPerc":"0.21%","MemUsage":"4.242MiB / 1.944GiB","Name":"registry","NetIO":"4.17kB / 0B","PIDs":"13"}