@danolez/docker-util 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/bin/docker-help +27 -0
- package/bin/docker-list +53 -0
- package/bin/docker-prune +188 -0
- package/bin/docker-stop +88 -0
- package/bin/docker-util +19 -0
- package/lib/common.sh +128 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fatunmbi Daniel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# docker-util
|
|
2
|
+
|
|
3
|
+
Small interactive helpers for people who run a lot of Docker Compose stacks
|
|
4
|
+
on one machine. You pick projects from a list instead of typing names, and
|
|
5
|
+
anything that is still running is left alone.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
docker-list Show compose projects and containers outside compose
|
|
9
|
+
docker-stop Pick running compose projects to stop, then pick which to delete
|
|
10
|
+
docker-prune Clean stopped containers, unused images, volumes and build cache
|
|
11
|
+
docker-help Show the command list
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Run it once without installing:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx @danolez/docker-util list
|
|
20
|
+
npx @danolez/docker-util stop
|
|
21
|
+
npx @danolez/docker-util prune --dry-run
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install it so the `docker-*` commands are on your PATH:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install -g @danolez/docker-util
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Straight from GitHub works too, no registry needed:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx github:danolez1/docker-util help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Works on macOS and Linux. It needs `bash` (the 3.2 that ships with macOS is
|
|
37
|
+
fine) and the `docker` CLI. Node is only used to install it.
|
|
38
|
+
|
|
39
|
+
## Picking things
|
|
40
|
+
|
|
41
|
+
Every choice is a menu. Nothing asks you to type a name or a number.
|
|
42
|
+
|
|
43
|
+
The tool uses [fzf](https://github.com/junegunn/fzf) if it is installed, then
|
|
44
|
+
[gum](https://github.com/charmbracelet/gum), and otherwise falls back to its
|
|
45
|
+
own arrow-key menu:
|
|
46
|
+
|
|
47
|
+
| key | action |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| up / down (or k / j) | move |
|
|
50
|
+
| space | mark or unmark (multi-select menus) |
|
|
51
|
+
| enter | confirm |
|
|
52
|
+
| q or esc | abort, nothing is changed |
|
|
53
|
+
|
|
54
|
+
In fzf and gum, multi-select menus start with a `(none)` row. Confirm on it
|
|
55
|
+
to pick nothing. Yes/No questions default to No.
|
|
56
|
+
|
|
57
|
+
Force a picker with `DOCKER_UTIL_PICKER=fzf|gum|builtin`.
|
|
58
|
+
|
|
59
|
+
## docker-stop
|
|
60
|
+
|
|
61
|
+
1. Lists every compose project with at least one running container. Mark the
|
|
62
|
+
ones to stop.
|
|
63
|
+
2. Stops them, then lists the same projects again so you can mark which ones
|
|
64
|
+
to delete. Deleting removes their containers and networks.
|
|
65
|
+
3. If you deleted anything, asks whether to keep or delete the project
|
|
66
|
+
volumes. Keeping them is the first option.
|
|
67
|
+
|
|
68
|
+
`docker-stop --dry-run` shows the commands without running them.
|
|
69
|
+
|
|
70
|
+
## docker-prune
|
|
71
|
+
|
|
72
|
+
Removes stopped containers, unused images, anonymous volumes and dangling
|
|
73
|
+
build cache. Compose projects with a running container are always kept, and
|
|
74
|
+
so are the images they use.
|
|
75
|
+
|
|
76
|
+
Before removing anything it asks:
|
|
77
|
+
|
|
78
|
+
- which stopped compose projects to keep,
|
|
79
|
+
- which unused named volumes to delete (none by default, since fixed-name
|
|
80
|
+
volumes often hold data you care about),
|
|
81
|
+
- whether to go ahead with the plan it prints.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
-n, --dry-run Show what would be removed, remove nothing
|
|
85
|
+
-y, --yes Skip prompts: remove all stopped projects, keep all named volumes
|
|
86
|
+
--keep-image REGEX Never remove images whose repo:tag matches REGEX (repeatable)
|
|
87
|
+
--cache-cap SIZE Build cache to keep, e.g. 10GB (default 20GB)
|
|
88
|
+
--tmp-prefix PREFIX Also delete $TMPDIR/PREFIX* dirs older than 24h (repeatable)
|
|
89
|
+
--no-mole Do not offer 'mo clean' afterwards
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If [Mole](https://github.com/tw93/Mole) is installed, it offers to run
|
|
93
|
+
`mo clean` at the end.
|
|
94
|
+
|
|
95
|
+
### Config
|
|
96
|
+
|
|
97
|
+
Defaults can live in `~/.config/docker-util/config` (or `$DOCKER_UTIL_CONFIG`).
|
|
98
|
+
It is plain shell:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
# Images you can't pull again, e.g. mirrors of deleted upstream tags
|
|
102
|
+
KEEP_IMAGE_PATTERN='^(registry\.example\.com/|myorg/)'
|
|
103
|
+
CACHE_CAP=20GB
|
|
104
|
+
# Scratch dirs your test suites leave in $TMPDIR
|
|
105
|
+
TMP_PREFIXES="myapp-test- e2e-run-"
|
|
106
|
+
TMP_MAX_AGE_MIN=1440
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`DOCKER_UTIL_KEEP_IMAGES`, `DOCKER_UTIL_CACHE_CAP` and
|
|
110
|
+
`DOCKER_UTIL_TMP_PREFIXES` override the file, and flags override both.
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
npm test # runs test/run.sh against a stub docker, no daemon needed
|
|
116
|
+
npm run lint # shellcheck
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/bin/docker-help
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
self="$0"
|
|
5
|
+
while [ -L "$self" ]; do
|
|
6
|
+
link=$(readlink "$self")
|
|
7
|
+
case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
|
|
8
|
+
done
|
|
9
|
+
# shellcheck source=../lib/common.sh
|
|
10
|
+
. "$(dirname "$self")/../lib/common.sh"
|
|
11
|
+
|
|
12
|
+
case "${1:-}" in -v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;; esac
|
|
13
|
+
|
|
14
|
+
cat <<EOF
|
|
15
|
+
docker-util $DOCKER_UTIL_VERSION
|
|
16
|
+
|
|
17
|
+
docker-list Show compose projects and containers outside compose
|
|
18
|
+
docker-stop Pick running compose projects to stop, then pick which to delete
|
|
19
|
+
docker-prune Clean stopped containers, unused images, volumes and build cache
|
|
20
|
+
(running compose projects are never touched)
|
|
21
|
+
docker-help Show this list
|
|
22
|
+
|
|
23
|
+
Each command also runs as 'docker-util <list|stop|prune|help>'.
|
|
24
|
+
Add --help to any command for its options. --dry-run works on stop and prune.
|
|
25
|
+
|
|
26
|
+
Picker in use: $(picker) (fzf, then gum, then a built-in arrow-key menu)
|
|
27
|
+
EOF
|
package/bin/docker-list
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Compose projects and loose containers at a glance.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
self="$0"
|
|
6
|
+
while [ -L "$self" ]; do
|
|
7
|
+
link=$(readlink "$self")
|
|
8
|
+
case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
|
|
9
|
+
done
|
|
10
|
+
# shellcheck source=../lib/common.sh
|
|
11
|
+
. "$(dirname "$self")/../lib/common.sh"
|
|
12
|
+
|
|
13
|
+
case "${1:-}" in
|
|
14
|
+
"") ;;
|
|
15
|
+
-h|--help)
|
|
16
|
+
cat <<EOF
|
|
17
|
+
docker-list $DOCKER_UTIL_VERSION
|
|
18
|
+
Show compose projects (running, stopped, partial) and containers outside compose.
|
|
19
|
+
|
|
20
|
+
Usage: docker-list
|
|
21
|
+
EOF
|
|
22
|
+
exit 0 ;;
|
|
23
|
+
-v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;;
|
|
24
|
+
*) echo "docker-list: unknown option '$1'" >&2; exit 2 ;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
require_docker
|
|
28
|
+
|
|
29
|
+
rows=$(docker ps -a --format '{{.Label "com.docker.compose.project"}} {{.State}} {{.Names}} {{.Image}} {{.Status}}')
|
|
30
|
+
|
|
31
|
+
bold "Compose projects"
|
|
32
|
+
printf '%s\n' "$rows" | awk -F'\t' '
|
|
33
|
+
$1 != "" {
|
|
34
|
+
total[$1]++
|
|
35
|
+
if ($2 == "running") up[$1]++
|
|
36
|
+
}
|
|
37
|
+
END {
|
|
38
|
+
n = 0
|
|
39
|
+
for (p in total) {
|
|
40
|
+
if (up[p] == total[p]) state = "running"
|
|
41
|
+
else if (up[p] > 0) state = "partial"
|
|
42
|
+
else state = "stopped"
|
|
43
|
+
printf " %-32s %-8s %d/%d up\n", p, state, up[p], total[p]
|
|
44
|
+
n++
|
|
45
|
+
}
|
|
46
|
+
if (n == 0) print " none"
|
|
47
|
+
}' | sort
|
|
48
|
+
|
|
49
|
+
echo
|
|
50
|
+
bold "Containers outside compose"
|
|
51
|
+
printf '%s\n' "$rows" | awk -F'\t' '
|
|
52
|
+
$1 == "" && $3 != "" { printf " %-32s %-8s %-30s %s\n", $3, $2, $4, $5; n++ }
|
|
53
|
+
END { if (n == 0) print " none" }'
|
package/bin/docker-prune
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Interactive Docker cleanup that never touches running compose stacks.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
self="$0"
|
|
6
|
+
while [ -L "$self" ]; do
|
|
7
|
+
link=$(readlink "$self")
|
|
8
|
+
case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
|
|
9
|
+
done
|
|
10
|
+
# shellcheck source=../lib/common.sh
|
|
11
|
+
. "$(dirname "$self")/../lib/common.sh"
|
|
12
|
+
|
|
13
|
+
CONFIG_FILE="${DOCKER_UTIL_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/docker-util/config}"
|
|
14
|
+
KEEP_IMAGE_PATTERN=""
|
|
15
|
+
CACHE_CAP="20GB"
|
|
16
|
+
TMP_PREFIXES=""
|
|
17
|
+
TMP_MAX_AGE_MIN=1440
|
|
18
|
+
MOLE=1
|
|
19
|
+
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
[ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE"
|
|
22
|
+
KEEP_IMAGE_PATTERN="${DOCKER_UTIL_KEEP_IMAGES:-$KEEP_IMAGE_PATTERN}"
|
|
23
|
+
CACHE_CAP="${DOCKER_UTIL_CACHE_CAP:-$CACHE_CAP}"
|
|
24
|
+
TMP_PREFIXES="${DOCKER_UTIL_TMP_PREFIXES:-$TMP_PREFIXES}"
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
cat <<EOF
|
|
28
|
+
docker-prune $DOCKER_UTIL_VERSION
|
|
29
|
+
Clean up stopped containers, unused images, volumes and build cache
|
|
30
|
+
without touching running compose projects.
|
|
31
|
+
|
|
32
|
+
Usage: docker-prune [options]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
-n, --dry-run Show what would be removed, remove nothing
|
|
36
|
+
-y, --yes Skip prompts: remove all stopped projects, keep all named volumes
|
|
37
|
+
--keep-image REGEX Never remove images whose repo:tag matches REGEX (repeatable)
|
|
38
|
+
--cache-cap SIZE Build cache to keep, e.g. 10GB (default: $CACHE_CAP)
|
|
39
|
+
--tmp-prefix PREFIX Also delete \$TMPDIR/PREFIX* dirs older than 24h (repeatable)
|
|
40
|
+
--no-mole Do not offer 'mo clean' afterwards
|
|
41
|
+
-h, --help Show this help
|
|
42
|
+
-v, --version Show version
|
|
43
|
+
|
|
44
|
+
Config file: $CONFIG_FILE
|
|
45
|
+
EOF
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
add_pattern() { KEEP_IMAGE_PATTERN="${KEEP_IMAGE_PATTERN:+$KEEP_IMAGE_PATTERN|}$1"; }
|
|
49
|
+
need_arg() { [ -n "${2:-}" ] || { echo "docker-prune: $1 needs a value" >&2; exit 2; }; }
|
|
50
|
+
|
|
51
|
+
while [ $# -gt 0 ]; do
|
|
52
|
+
case "$1" in
|
|
53
|
+
-n|--dry-run) DRY_RUN=1 ;;
|
|
54
|
+
-y|--yes) ASSUME_YES=1 ;;
|
|
55
|
+
--keep-image) need_arg "$1" "${2:-}"; add_pattern "$2"; shift ;;
|
|
56
|
+
--keep-image=*) add_pattern "${1#*=}" ;;
|
|
57
|
+
--cache-cap) need_arg "$1" "${2:-}"; CACHE_CAP="$2"; shift ;;
|
|
58
|
+
--cache-cap=*) CACHE_CAP="${1#*=}" ;;
|
|
59
|
+
--tmp-prefix) need_arg "$1" "${2:-}"; TMP_PREFIXES="$TMP_PREFIXES $2"; shift ;;
|
|
60
|
+
--tmp-prefix=*) TMP_PREFIXES="$TMP_PREFIXES ${1#*=}" ;;
|
|
61
|
+
--no-mole) MOLE=0 ;;
|
|
62
|
+
-h|--help) usage; exit 0 ;;
|
|
63
|
+
-v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;;
|
|
64
|
+
*) echo "docker-prune: unknown option '$1'" >&2; usage >&2; exit 2 ;;
|
|
65
|
+
esac
|
|
66
|
+
shift
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
aborted() { echo "Aborted, nothing removed." >&2; exit 130; }
|
|
70
|
+
|
|
71
|
+
require_docker
|
|
72
|
+
|
|
73
|
+
bold "Docker usage before"
|
|
74
|
+
docker system df
|
|
75
|
+
|
|
76
|
+
running_projects=$(compose_projects)
|
|
77
|
+
all_projects=$(compose_projects -a)
|
|
78
|
+
stopped_projects=$(comm -23 <(printf '%s\n' "$all_projects") <(printf '%s\n' "$running_projects"))
|
|
79
|
+
|
|
80
|
+
echo
|
|
81
|
+
bold "Running compose projects (always kept)"
|
|
82
|
+
# shellcheck disable=SC2086
|
|
83
|
+
printf ' %s\n' ${running_projects:-none}
|
|
84
|
+
|
|
85
|
+
kept_projects=""
|
|
86
|
+
if [ -n "$stopped_projects" ]; then
|
|
87
|
+
echo
|
|
88
|
+
kept_projects=$(pick_many "Stopped compose projects to KEEP" "$stopped_projects") || aborted
|
|
89
|
+
fi
|
|
90
|
+
protected_projects=$(printf '%s\n%s\n' "$running_projects" "$kept_projects" | sed '/^$/d' | sort -u)
|
|
91
|
+
doomed_projects=$(comm -23 <(printf '%s\n' "$stopped_projects" | sed '/^$/d') <(printf '%s\n' "$protected_projects"))
|
|
92
|
+
|
|
93
|
+
doomed_containers=""
|
|
94
|
+
while IFS=$'\t' read -r id name project; do
|
|
95
|
+
[ -z "$id" ] && continue
|
|
96
|
+
[ -n "$project" ] && in_list "$project" "$protected_projects" && continue
|
|
97
|
+
doomed_containers+="$id $name ${project:-(no compose project)}"$'\n'
|
|
98
|
+
done < <(docker ps -a --filter status=exited --filter status=created --filter status=dead \
|
|
99
|
+
--format '{{.ID}}\t{{.Names}}\t{{.Label "com.docker.compose.project"}}')
|
|
100
|
+
|
|
101
|
+
container_ids=$(printf '%s' "$doomed_containers" | cut -f1 | sed '/^$/d')
|
|
102
|
+
# Named volumes are only deleted when picked; fixed-name ones can hold backups.
|
|
103
|
+
unused_volumes=""
|
|
104
|
+
while IFS= read -r vol; do
|
|
105
|
+
[ -z "$vol" ] && continue
|
|
106
|
+
# Anonymous volumes are handled by `volume prune` below.
|
|
107
|
+
[[ "$vol" =~ ^[0-9a-f]{64}$ ]] && continue
|
|
108
|
+
users=$(docker ps -aq --filter volume="$vol" | grep -vxF -f <(printf '%s\n' "$container_ids"))
|
|
109
|
+
[ -z "$users" ] && unused_volumes+="$vol"$'\n'
|
|
110
|
+
done < <(docker volume ls --format '{{.Name}}')
|
|
111
|
+
unused_volumes=$(printf '%s' "$unused_volumes" | sed '/^$/d')
|
|
112
|
+
|
|
113
|
+
doomed_volumes=""
|
|
114
|
+
if [ -n "$unused_volumes" ]; then
|
|
115
|
+
echo
|
|
116
|
+
doomed_volumes=$(pick_many "Unused named volumes to DELETE (may be backups)" "$unused_volumes") || aborted
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
echo
|
|
120
|
+
bold "Plan"
|
|
121
|
+
echo "Stopped projects to remove:"
|
|
122
|
+
# shellcheck disable=SC2086
|
|
123
|
+
printf ' %s\n' ${doomed_projects:-none}
|
|
124
|
+
echo "Stopped containers to remove:"
|
|
125
|
+
printf '%s' "${doomed_containers:- none
|
|
126
|
+
}" | awk -F'\t' 'NF>1 {printf " %s (%s)\n", $2, $3} NF<=1 {print}'
|
|
127
|
+
echo "Named volumes to remove:"
|
|
128
|
+
# shellcheck disable=SC2086
|
|
129
|
+
printf ' %s\n' ${doomed_volumes:-none}
|
|
130
|
+
echo "Also: anonymous unused volumes, dangling images, unused images"
|
|
131
|
+
echo " (except images of kept projects${KEEP_IMAGE_PATTERN:+ and $KEEP_IMAGE_PATTERN}),"
|
|
132
|
+
echo " build cache trimmed to $CACHE_CAP."
|
|
133
|
+
[ -n "${TMP_PREFIXES// /}" ] && echo " \$TMPDIR dirs older than ${TMP_MAX_AGE_MIN}m matching:$TMP_PREFIXES"
|
|
134
|
+
|
|
135
|
+
if [ "$DRY_RUN" = 0 ] && ! confirm "Proceed?"; then
|
|
136
|
+
echo "Nothing removed."
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo
|
|
141
|
+
bold "Removing"
|
|
142
|
+
# shellcheck disable=SC2086
|
|
143
|
+
[ -n "$container_ids" ] && run docker rm -v $container_ids >/dev/null
|
|
144
|
+
for vol in $doomed_volumes; do
|
|
145
|
+
run docker volume rm "$vol" >/dev/null
|
|
146
|
+
done
|
|
147
|
+
run docker volume prune -f
|
|
148
|
+
|
|
149
|
+
# Containers pending removal are excluded so a dry run lists the same images a real run removes.
|
|
150
|
+
used_images=$(docker ps -aq | grep -vxF -f <(printf '%s\n' "$container_ids") |
|
|
151
|
+
xargs docker inspect 2>/dev/null --format '{{.Image}}' | sort -u)
|
|
152
|
+
while IFS=$'\t' read -r id ref; do
|
|
153
|
+
[ -z "$id" ] && continue
|
|
154
|
+
in_list "$id" "$used_images" && continue
|
|
155
|
+
# An empty regex matches everything, so it must mean "keep nothing extra".
|
|
156
|
+
[ -n "$KEEP_IMAGE_PATTERN" ] && [[ "$ref" =~ $KEEP_IMAGE_PATTERN ]] && continue
|
|
157
|
+
[ "$ref" = "<none>:<none>" ] && continue
|
|
158
|
+
# Digest-pinned pulls have no tag, so only their ID addresses them.
|
|
159
|
+
[[ "$ref" == *:"<none>" ]] && ref="$id"
|
|
160
|
+
run docker image rm "$ref" >/dev/null 2>&1
|
|
161
|
+
done < <(docker image ls --no-trunc --format '{{.ID}}\t{{.Repository}}:{{.Tag}}')
|
|
162
|
+
run docker image prune -f >/dev/null
|
|
163
|
+
|
|
164
|
+
# --keep-storage was renamed to --max-used-space in newer buildx.
|
|
165
|
+
if docker builder prune --help 2>&1 | grep -q -- --max-used-space; then
|
|
166
|
+
run docker builder prune -af --max-used-space "$CACHE_CAP"
|
|
167
|
+
else
|
|
168
|
+
run docker builder prune -af --keep-storage "$CACHE_CAP"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
tmp_root="${TMPDIR:-/tmp}"
|
|
172
|
+
for prefix in $TMP_PREFIXES; do
|
|
173
|
+
find "$tmp_root" -maxdepth 1 -type d -name "${prefix}*" -mmin +"$TMP_MAX_AGE_MIN" -print0 2>/dev/null |
|
|
174
|
+
while IFS= read -r -d '' dir; do run rm -rf "$dir"; done
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
echo
|
|
178
|
+
bold "Docker usage after"
|
|
179
|
+
docker system df
|
|
180
|
+
|
|
181
|
+
if [ "$MOLE" = 1 ] && command -v mo >/dev/null 2>&1; then
|
|
182
|
+
echo
|
|
183
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
184
|
+
dim " would offer: mo clean (macOS caches, logs, temp)"
|
|
185
|
+
elif [ "$ASSUME_YES" = 0 ] && confirm "Also clean macOS caches and temp with Mole (mo clean)?"; then
|
|
186
|
+
mo clean
|
|
187
|
+
fi
|
|
188
|
+
fi
|
package/bin/docker-stop
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop running compose projects picked from a list, then optionally delete them.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
self="$0"
|
|
6
|
+
while [ -L "$self" ]; do
|
|
7
|
+
link=$(readlink "$self")
|
|
8
|
+
case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
|
|
9
|
+
done
|
|
10
|
+
# shellcheck source=../lib/common.sh
|
|
11
|
+
. "$(dirname "$self")/../lib/common.sh"
|
|
12
|
+
|
|
13
|
+
usage() {
|
|
14
|
+
cat <<EOF
|
|
15
|
+
docker-stop $DOCKER_UTIL_VERSION
|
|
16
|
+
Pick running compose projects to stop, then pick which of those to delete.
|
|
17
|
+
|
|
18
|
+
Usage: docker-stop [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-n, --dry-run Show what would happen, change nothing
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
-v, --version Show version
|
|
24
|
+
EOF
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
while [ $# -gt 0 ]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
-n|--dry-run) DRY_RUN=1 ;;
|
|
30
|
+
-h|--help) usage; exit 0 ;;
|
|
31
|
+
-v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;;
|
|
32
|
+
*) echo "docker-stop: unknown option '$1'" >&2; usage >&2; exit 2 ;;
|
|
33
|
+
esac
|
|
34
|
+
shift
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
aborted() { echo "Aborted, nothing $1." >&2; exit 130; }
|
|
38
|
+
by_project() { printf 'label=com.docker.compose.project=%s' "$1"; }
|
|
39
|
+
|
|
40
|
+
require_docker
|
|
41
|
+
|
|
42
|
+
running=$(compose_projects)
|
|
43
|
+
if [ -z "$running" ]; then
|
|
44
|
+
echo "No running compose projects."
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
chosen=$(pick_many "Compose projects to STOP" "$running") || aborted "stopped"
|
|
49
|
+
if [ -z "$chosen" ]; then
|
|
50
|
+
echo "Nothing stopped."
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
bold "Stopping"
|
|
55
|
+
for project in $chosen; do
|
|
56
|
+
echo " $project"
|
|
57
|
+
# shellcheck disable=SC2046
|
|
58
|
+
run docker stop $(docker ps -q --filter "$(by_project "$project")") >/dev/null
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
echo
|
|
62
|
+
doomed=$(pick_many "Stopped projects to also DELETE (containers and networks)" "$chosen") || aborted "deleted"
|
|
63
|
+
if [ -z "$doomed" ]; then
|
|
64
|
+
echo "Stopped, nothing deleted."
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Volumes hold the data, so wiping them is a separate, explicit choice.
|
|
69
|
+
volumes=$(pick_one "Volumes of deleted projects" "Keep volumes" "Delete volumes too") || aborted "deleted"
|
|
70
|
+
|
|
71
|
+
echo
|
|
72
|
+
bold "Deleting"
|
|
73
|
+
for project in $doomed; do
|
|
74
|
+
echo " $project"
|
|
75
|
+
filter=$(by_project "$project")
|
|
76
|
+
ids=$(docker ps -aq --filter "$filter")
|
|
77
|
+
# shellcheck disable=SC2086
|
|
78
|
+
[ -n "$ids" ] && run docker rm -v $ids >/dev/null
|
|
79
|
+
nets=$(docker network ls -q --filter "$filter")
|
|
80
|
+
# shellcheck disable=SC2086
|
|
81
|
+
[ -n "$nets" ] && run docker network rm $nets >/dev/null
|
|
82
|
+
if [ "$volumes" = "Delete volumes too" ]; then
|
|
83
|
+
vols=$(docker volume ls -q --filter "$filter")
|
|
84
|
+
# shellcheck disable=SC2086
|
|
85
|
+
[ -n "$vols" ] && run docker volume rm $vols >/dev/null
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
echo "Done."
|
package/bin/docker-util
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Single entry point so `npx @danolez/docker-util <cmd>` reaches every command.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
self="$0"
|
|
6
|
+
while [ -L "$self" ]; do
|
|
7
|
+
link=$(readlink "$self")
|
|
8
|
+
case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
|
|
9
|
+
done
|
|
10
|
+
dir="$(dirname "$self")"
|
|
11
|
+
|
|
12
|
+
cmd="${1:-help}"
|
|
13
|
+
[ $# -gt 0 ] && shift
|
|
14
|
+
case "$cmd" in
|
|
15
|
+
list|stop|prune|help) exec "$dir/docker-$cmd" "$@" ;;
|
|
16
|
+
-h|--help) exec "$dir/docker-help" ;;
|
|
17
|
+
-v|--version) exec "$dir/docker-help" --version ;;
|
|
18
|
+
*) echo "docker-util: unknown command '$cmd'" >&2; "$dir/docker-help" >&2; exit 2 ;;
|
|
19
|
+
esac
|
package/lib/common.sh
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# Shared by every docker-util command so they all pick things the same way.
|
|
3
|
+
|
|
4
|
+
# shellcheck disable=SC2034
|
|
5
|
+
DOCKER_UTIL_VERSION="1.0.0"
|
|
6
|
+
NONE="(none)"
|
|
7
|
+
DRY_RUN="${DRY_RUN:-0}"
|
|
8
|
+
ASSUME_YES="${ASSUME_YES:-0}"
|
|
9
|
+
|
|
10
|
+
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
11
|
+
dim() { printf '\033[2m%s\033[0m\n' "$*"; }
|
|
12
|
+
# fd 3 keeps dry-run output visible past callers that silence the real command.
|
|
13
|
+
exec 3>&1
|
|
14
|
+
run() {
|
|
15
|
+
if [ "$DRY_RUN" = 1 ]; then dim " would run: $*" >&3; else "$@"; fi
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
in_list() { printf '%s\n' "$2" | grep -qxF -- "$1"; }
|
|
19
|
+
|
|
20
|
+
require_docker() {
|
|
21
|
+
command -v docker >/dev/null 2>&1 || { echo "docker not found on PATH." >&2; exit 1; }
|
|
22
|
+
docker info >/dev/null 2>&1 || { echo "Docker daemon not reachable." >&2; exit 1; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Opened once so every menu in a run shares one read offset (lets tests script keystrokes).
|
|
26
|
+
{ exec 4<"${DOCKER_UTIL_TTY:-/dev/tty}"; } 2>/dev/null || true
|
|
27
|
+
|
|
28
|
+
picker() {
|
|
29
|
+
local want="${DOCKER_UTIL_PICKER:-auto}"
|
|
30
|
+
if [ "$want" = auto ]; then
|
|
31
|
+
if command -v fzf >/dev/null 2>&1; then want=fzf
|
|
32
|
+
elif command -v gum >/dev/null 2>&1; then want=gum
|
|
33
|
+
else want=builtin; fi
|
|
34
|
+
fi
|
|
35
|
+
echo "$want"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Arrow-key menu for machines without fzf or gum. Args: multi(0|1) prompt items...
|
|
39
|
+
_menu() {
|
|
40
|
+
local multi="$1" prompt="$2"; shift 2
|
|
41
|
+
local opts=("$@") n=$# cur=0 i key rest mark ptr hint drawn=0
|
|
42
|
+
local sel=()
|
|
43
|
+
[ "$n" -eq 0 ] && return 0
|
|
44
|
+
# Runs in a $() subshell, so this only restores the cursor for this menu.
|
|
45
|
+
trap 'printf "\033[?25h" >&2' EXIT
|
|
46
|
+
for ((i = 0; i < n; i++)); do sel[i]=0; done
|
|
47
|
+
hint="up/down move, enter select, q abort"
|
|
48
|
+
[ "$multi" = 1 ] && hint="up/down move, space mark, enter confirm, q abort"
|
|
49
|
+
printf '\033[?25l\033[1m%s\033[0m \033[2m%s\033[0m\n' "$prompt" "$hint" >&2
|
|
50
|
+
while :; do
|
|
51
|
+
[ "$drawn" = 1 ] && printf '\033[%dA' "$n" >&2
|
|
52
|
+
for ((i = 0; i < n; i++)); do
|
|
53
|
+
ptr=" "; [ "$i" = "$cur" ] && ptr="> "
|
|
54
|
+
mark=""
|
|
55
|
+
if [ "$multi" = 1 ]; then mark="[ ] "; [ "${sel[i]}" = 1 ] && mark="[x] "; fi
|
|
56
|
+
printf '\033[2K%s%s%s\n' "$ptr" "$mark" "${opts[i]}" >&2
|
|
57
|
+
done
|
|
58
|
+
drawn=1
|
|
59
|
+
IFS= read -rsn1 key 2>/dev/null <&4 || key=q
|
|
60
|
+
if [ "$key" = $'\033' ]; then
|
|
61
|
+
rest=""
|
|
62
|
+
IFS= read -rsn2 -t 1 rest 2>/dev/null <&4
|
|
63
|
+
case "$rest" in "[A") key=k ;; "[B") key=j ;; *) key=q ;; esac
|
|
64
|
+
fi
|
|
65
|
+
case "$key" in
|
|
66
|
+
k) cur=$(((cur - 1 + n) % n)) ;;
|
|
67
|
+
j) cur=$(((cur + 1) % n)) ;;
|
|
68
|
+
" ") [ "$multi" = 1 ] && sel[cur]=$((1 - sel[cur])) ;;
|
|
69
|
+
"") break ;;
|
|
70
|
+
q) printf '\033[?25h' >&2; return 130 ;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
printf '\033[?25h' >&2
|
|
74
|
+
if [ "$multi" = 1 ]; then
|
|
75
|
+
for ((i = 0; i < n; i++)); do [ "${sel[i]}" = 1 ] && printf '%s\n' "${opts[i]}"; done
|
|
76
|
+
else
|
|
77
|
+
printf '%s\n' "${opts[cur]}"
|
|
78
|
+
fi
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Newline-separated items in, picked items out. Non-zero only on abort.
|
|
83
|
+
pick_many() {
|
|
84
|
+
local prompt="$1" items="$2" picked rc line
|
|
85
|
+
local arr=()
|
|
86
|
+
[ -z "$items" ] && return 0
|
|
87
|
+
[ "$ASSUME_YES" = 1 ] && return 0
|
|
88
|
+
case "$(picker)" in
|
|
89
|
+
# fzf and gum return the highlighted row on a bare ENTER, so (none) is the only way to pick nothing.
|
|
90
|
+
fzf)
|
|
91
|
+
picked=$(printf '%s\n%s\n' "$NONE" "$items" | fzf --multi --prompt="$prompt> " \
|
|
92
|
+
--height=40% --reverse --header="TAB to mark, ENTER to confirm, ESC to abort")
|
|
93
|
+
rc=$? ;;
|
|
94
|
+
gum)
|
|
95
|
+
picked=$(printf '%s\n%s\n' "$NONE" "$items" | gum choose --no-limit --header="$prompt")
|
|
96
|
+
rc=$? ;;
|
|
97
|
+
*)
|
|
98
|
+
while IFS= read -r line; do [ -n "$line" ] && arr+=("$line"); done <<<"$items"
|
|
99
|
+
[ ${#arr[@]} -eq 0 ] && return 0
|
|
100
|
+
picked=$(_menu 1 "$prompt" "${arr[@]}")
|
|
101
|
+
rc=$? ;;
|
|
102
|
+
esac
|
|
103
|
+
[ "$rc" -ne 0 ] && return 130
|
|
104
|
+
printf '%s\n' "$picked" | grep -vxF -- "$NONE" | sed '/^$/d'
|
|
105
|
+
return 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Items as arguments, one picked item out. Non-zero only on abort.
|
|
109
|
+
pick_one() {
|
|
110
|
+
local prompt="$1"; shift
|
|
111
|
+
case "$(picker)" in
|
|
112
|
+
fzf) printf '%s\n' "$@" | fzf --prompt="$prompt> " --height=40% --reverse \
|
|
113
|
+
--header="ENTER to select, ESC to abort" ;;
|
|
114
|
+
gum) gum choose --header="$prompt" "$@" ;;
|
|
115
|
+
*) _menu 0 "$prompt" "$@" ;;
|
|
116
|
+
esac
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
confirm() {
|
|
120
|
+
local answer
|
|
121
|
+
[ "$ASSUME_YES" = 1 ] && return 0
|
|
122
|
+
answer=$(pick_one "$1" No Yes) || return 1
|
|
123
|
+
[ "$answer" = Yes ]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
compose_projects() {
|
|
127
|
+
docker ps "$@" --format '{{.Label "com.docker.compose.project"}}' | sed '/^$/d' | sort -u
|
|
128
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@danolez/docker-util",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive Docker helpers: list, stop and prune compose projects without typing names",
|
|
5
|
+
"bin": {
|
|
6
|
+
"docker-util": "bin/docker-util",
|
|
7
|
+
"docker-list": "bin/docker-list",
|
|
8
|
+
"docker-stop": "bin/docker-stop",
|
|
9
|
+
"docker-prune": "bin/docker-prune",
|
|
10
|
+
"docker-help": "bin/docker-help"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "bash test/run.sh",
|
|
18
|
+
"lint": "shellcheck bin/* lib/common.sh test/run.sh test/stub/docker"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"docker",
|
|
22
|
+
"compose",
|
|
23
|
+
"prune",
|
|
24
|
+
"stop",
|
|
25
|
+
"cleanup",
|
|
26
|
+
"cli",
|
|
27
|
+
"fzf"
|
|
28
|
+
],
|
|
29
|
+
"os": [
|
|
30
|
+
"darwin",
|
|
31
|
+
"linux"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/danolez1/docker-util.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/danolez1/docker-util#readme",
|
|
38
|
+
"bugs": "https://github.com/danolez1/docker-util/issues",
|
|
39
|
+
"author": "Fatunmbi Daniel",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|