Skip to content

Instantly share code, notes, and snippets.

@oleg-koval
Last active September 1, 2025 12:36
Show Gist options
  • Save oleg-koval/b191723df11ed735cbba16fa83999aa1 to your computer and use it in GitHub Desktop.
Save oleg-koval/b191723df11ed735cbba16fa83999aa1 to your computer and use it in GitHub Desktop.
dclean = one command to clean, rebuild, and start selected Docker Compose services.

dclean

dclean = one command to clean, rebuild, and start selected Docker Compose services. It removes service containers πŸ“¦, named volumes πŸ—„οΈ, and images πŸ–ΌοΈ used by those servicesβ€”then runs build + up -d.

  • Works with Compose v2 (docker compose …) and legacy v1 (docker-compose …)
  • Written for zsh (macOS/Linux)
  • Friendly, emoji-rich output 😎

Quick install

Choose one:

A) Stand-alone command (to ~/.local/bin)

sh -c "$(curl -fsSL https://gist.githubusercontent.com/oleg-koval/b191723df11ed735cbba16fa83999aa1/raw/install.sh)" -- --bin

then open a new terminal, or:

source ~/.zshrc

B) Oh My Zsh plugin

sh -c "$(curl -fsSL https://gist.githubusercontent.com/oleg-koval/b191723df11ed735cbba16fa83999aa1/raw/install.sh)" -- --plugin

add 'dclean' to plugins=(...) in ~/.zshrc, then: source ~/.zshrc

Custom bin dir:

DCLEAN_BIN_DIR="$HOME/bin" sh -c "$(curl -fsSL https://gist.githubusercontent.com/oleg-koval/b191723df11ed735cbba16fa83999aa1/raw/install.sh)" -- --bin

Requirements

β€’	Docker + Docker Compose
β€’	zsh (default on macOS; installable on Linux)  (oh-my-zsh plugin) 
β€’	Run dclean inside a Compose project (where your compose file(s) live) (docker-compose.yml)

Usage

From any Compose project directory:

Clean + rebuild + start only these services

dclean api users admin

No args β†’ apply to all services in the project

dclean

What happens

1.	Prints containers to remove (πŸ“¦).
2.	Removes named volumes attached to those containers (πŸ—„οΈ).
3.	Removes the service images if present (πŸ–ΌοΈ).
4.	docker compose build the services.
5.	docker compose up -d the services.

Uninstall

Bin mode

rm -f ~/.local/bin/dclean    # or your DCLEAN_BIN_DIR

(Optionally remove the PATH line the installer added to ~/.zshrc.)

Plugin mode

rm -rf ~/.oh-my-zsh/custom/plugins/dclean
# remove 'dclean' from plugins=(...) in ~/.zshrc, then:
source ~/.zshrc

Troubleshooting

β€œdocker compose not found.”

Ensure Docker Desktop (or Docker Engine + Compose plugin) is installed. If you only have v1, ensure docker-compose is on PATH.

β€œpermission denied” (bin mode)

chmod +x ~/.local/bin/dclean

and make sure ~/.local/bin is on PATH:

echo $PATH | tr ':' '\n'

Zsh too old

The script uses some zsh niceties. If /usr/bin/env zsh is ancient, update zsh.

brew install zsh

License

MIT. Have fun.

Credits

Standalone script

Install

Store it in .zshrc and source ~/.zshrc

Run

dclean service1 service2

Source code

# Clean, rebuild, and start specific Docker Compose services (zsh-native, fixed)
dclean() {
  emulate -L zsh -o pipefail

  # Allow running from anywhere via envs
  local project_dir="${DCLEAN_PROJECT_DIR:-$PWD}"

  # Change to project directory
  cd "$project_dir" || {
    echo "[x] Cannot access project directory: $project_dir"
    return 1
  }

  # Check if this is a Docker Compose project and determine if profiles are needed
  local use_profiles=false

  # Check if there are services without profiles
  local services_count=$(docker compose config --services 2>/dev/null | wc -l)
  if [[ "$services_count" -eq 0 ]]; then
    use_profiles=true
  fi

  # Build docker compose command
  local dc_cmd="docker compose"
  if [[ "$use_profiles" == "true" ]]; then
    dc_cmd="docker compose --profile all --profile backend --profile frontend"
  fi

  # Get services list
  local -a services
  if (( $# == 0 )); then
    services=(${(f)$(eval "$dc_cmd" config --services)})
  else
    services=("$@")
  fi

  if [[ ${#services[@]} -eq 0 ]]; then
    echo "[x] No services found or specified."
    return 1
  fi

  echo "🎯  Target services:"; printf "  -  %s\n" "${services[@]}"; echo "──────────────────────────────"

  # Get running containers for these services
  local -a cids svc_cids
  local s; for s in "${services[@]}"; do
    svc_cids=(${(f)$(eval "$dc_cmd" ps -q "$s" 2>/dev/null)})
    (( ${#svc_cids[@]} )) && cids+=("${svc_cids[@]}")
  done

  # Get container names
  local -a cname; local cid name
  for cid in "${cids[@]}"; do
    name="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null)"; name="${name#/}"
    [[ -n "$name" ]] && cname+=("$name")
  done

  # Get volumes and images from containers
  local -a vols imgs; local img_id v
  for cid in "${cids[@]}"; do
    for v in ${(f)$(docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "$cid" 2>/dev/null)}; do
      [[ -n "$v" ]] && vols+=("$v")
    done
    img_id="$(docker inspect -f '{{.Image}}' "$cid" 2>/dev/null)"
    [[ -n "$img_id" ]] && imgs+=("$img_id")
  done

  local -a uniq_vols uniq_imgs
  uniq_vols=(${(u)vols}); uniq_imgs=(${(u)imgs})

  echo "πŸ—‘οΈ  Removing containers..."; echo "──────────────────────────────"
  if (( ${#cname[@]} )); then printf '  πŸ“¦ %s\n' "${cname[@]}"; else echo "No containers to remove."; fi
  eval "$dc_cmd" rm -sfv "${services[@]}" >/dev/null 2>&1 || true

  echo "──────────────────────────────"; echo "πŸ—‘οΈ  Removing volumes..."; echo "──────────────────────────────"
  if (( ${#uniq_vols[@]} )); then printf '  πŸ—„οΈ  %s\n' "${uniq_vols[@]}"; docker volume rm -f "${uniq_vols[@]}" >/dev/null 2>&1 || true
  else echo "No volumes to remove."; fi

  echo "──────────────────────────────"; echo "πŸ—‘οΈ  Removing images..."; echo "──────────────────────────────"
  if (( ${#uniq_imgs[@]} )); then
    local img ref short
    for img in "${uniq_imgs[@]}"; do
      short="${${img#sha256:}:0:12}"
      ref="$(docker image inspect --format '{{if .RepoTags}}{{index .RepoTags 0}}{{else}}<untagged>{{end}}' "$img" 2>/dev/null)"
      echo "  πŸ–ΌοΈ  ${short:-$img} ${ref}"
    done
    docker image rm -f "${uniq_imgs[@]}" >/dev/null 2>&1 || true
  else echo "No images to remove."; fi

  echo "πŸ”„  Rebuilding services..."; echo "──────────────────────────────"
  echo "πŸ› οΈ  Building Docker Compose services..."; echo "──────────────────────────────"
  if eval "$dc_cmd" build "${services[@]}"; then echo "πŸŽ‰  All services built successfully!"; else echo "❌  One or more services failed to build."; fi

  echo "πŸš€  Starting services..."; echo "──────────────────────────────"
  eval "$dc_cmd" up -d "${services[@]}"; echo "[βœ“] Done."
}
#!/usr/bin/env sh
set -eu
MODE="${1:---bin}" # default to --bin
ZSHRC="${ZDOTDIR:-$HOME}/.zshrc"
case "$MODE" in
--bin)
BIN_DIR="${DCLEAN_BIN_DIR:-$HOME/.local/bin}"
TARGET="$BIN_DIR/dclean"
echo "β†’ Installing dclean command to: $TARGET"
mkdir -p "$BIN_DIR"
# write the executable
cat > "$TARGET" <<'ZSH'
#!/usr/bin/env zsh
# dclean: clean, rebuild, and start specific Docker Compose services
dclean() {
emulate -L zsh -o pipefail
# Allow running from anywhere via envs
local project_dir="${DCLEAN_PROJECT_DIR:-$PWD}"
# Change to project directory
cd "$project_dir" || {
echo "[x] Cannot access project directory: $project_dir"
return 1
}
# Check if this is a Docker Compose project and determine if profiles are needed
local use_profiles=false
# Check if there are services without profiles
local services_count=$(docker compose config --services 2>/dev/null | wc -l)
if [[ "$services_count" -eq 0 ]]; then
use_profiles=true
fi
# Build docker compose command
local dc_cmd="docker compose"
if [[ "$use_profiles" == "true" ]]; then
dc_cmd="docker compose --profile all --profile backend --profile frontend"
fi
# Get services list
local -a services
if (( $# == 0 )); then
services=(${(f)$(eval "$dc_cmd" config --services)})
else
services=("$@")
fi
if [[ ${#services[@]} -eq 0 ]]; then
echo "[x] No services found or specified."
return 1
fi
echo "🎯 Target services:"; printf " - %s\n" "${services[@]}"; echo "──────────────────────────────"
# Get running containers for these services
local -a cids svc_cids
local s; for s in "${services[@]}"; do
svc_cids=(${(f)$(eval "$dc_cmd" ps -q "$s" 2>/dev/null)})
(( ${#svc_cids[@]} )) && cids+=("${svc_cids[@]}")
done
# Get container names
local -a cname; local cid name
for cid in "${cids[@]}"; do
name="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null)"; name="${name#/}"
[[ -n "$name" ]] && cname+=("$name")
done
# Get volumes and images from containers
local -a vols imgs; local img_id v
for cid in "${cids[@]}"; do
for v in ${(f)$(docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "$cid" 2>/dev/null)}; do
[[ -n "$v" ]] && vols+=("$v")
done
img_id="$(docker inspect -f '{{.Image}}' "$cid" 2>/dev/null)"
[[ -n "$img_id" ]] && imgs+=("$img_id")
done
local -a uniq_vols uniq_imgs
uniq_vols=(${(u)vols}); uniq_imgs=(${(u)imgs})
echo "πŸ—‘οΈ Removing containers..."; echo "──────────────────────────────"
if (( ${#cname[@]} )); then printf ' πŸ“¦ %s\n' "${cname[@]}"; else echo "No containers to remove."; fi
eval "$dc_cmd" rm -sfv "${services[@]}" >/dev/null 2>&1 || true
echo "──────────────────────────────"; echo "πŸ—‘οΈ Removing volumes..."; echo "──────────────────────────────"
if (( ${#uniq_vols[@]} )); then printf ' πŸ—„οΈ %s\n' "${uniq_vols[@]}"; docker volume rm -f "${uniq_vols[@]}" >/dev/null 2>&1 || true
else echo "No volumes to remove."; fi
echo "──────────────────────────────"; echo "πŸ—‘οΈ Removing images..."; echo "──────────────────────────────"
if (( ${#uniq_imgs[@]} )); then
local img ref short
for img in "${uniq_imgs[@]}"; do
short="${${img#sha256:}:0:12}"
ref="$(docker image inspect --format '{{if .RepoTags}}{{index .RepoTags 0}}{{else}}<untagged>{{end}}' "$img" 2>/dev/null)"
echo " πŸ–ΌοΈ ${short:-$img} ${ref}"
done
docker image rm -f "${uniq_imgs[@]}" >/dev/null 2>&1 || true
else echo "No images to remove."; fi
echo "πŸ”„ Rebuilding services..."; echo "──────────────────────────────"
echo "πŸ› οΈ Building Docker Compose services..."; echo "──────────────────────────────"
if eval "$dc_cmd" build "${services[@]}"; then echo "πŸŽ‰ All services built successfully!"; else echo "❌ One or more services failed to build."; fi
echo "πŸš€ Starting services..."; echo "──────────────────────────────"
eval "$dc_cmd" up -d "${services[@]}"; echo "[βœ“] Done."
}
dclean "$@"
ZSH
chmod +x "$TARGET"
# ensure PATH contains ~/.local/bin
case ":${PATH:-}:" in *":$BIN_DIR:"*) ;; *)
printf '\n# dclean installer: add user bin to PATH\nexport PATH="%s:$PATH"\n' "$BIN_DIR" >> "$ZSHRC"
echo "β†’ Added $BIN_DIR to PATH in $ZSHRC (reload shell to use 'dclean')."
esac
echo "βœ“ Installed. Use: dclean <service> [service…]"
;;
--plugin)
ZSH_DIR="${ZSH:-$HOME/.oh-my-zsh}"
PLUGIN_DIR="$ZSH_DIR/custom/plugins/dclean"
[ -d "$ZSH_DIR" ] || { echo "❌ Oh My Zsh not found at $ZSH_DIR"; exit 1; }
echo "β†’ Installing Oh My Zsh plugin to: $PLUGIN_DIR"
mkdir -p "$PLUGIN_DIR"
cat > "$PLUGIN_DIR/dclean.plugin.zsh" <<'ZPLUG'
# dclean: clean, rebuild, and start specific Docker Compose services
# Usage: dclean [service ...] (no args β†’ all services)
dclean() {
emulate -L zsh
setopt pipefail no_unset
local dc
if docker compose version >/dev/null 2>&1; then
dc="docker compose"
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
dc="docker-compose"
else
echo "[x] docker compose not found."
return 1
fi
$dc config >/dev/null 2>&1 || { echo "[x] Not a Docker Compose project directory."; return 1; }
local -a services
if (( $# == 0 )); then services=(${(f)$($dc config --services)}); else services=("$@"); fi
echo "🎯 Target services:"; printf " - %s\n" "${services[@]}"; echo "──────────────────────────────"
local -a cids svc_cids
local s; for s in "${services[@]}"; do
svc_cids=(${(f)$($dc ps -q "$s" 2>/dev/null)})
(( ${#svc_cids[@]} )) && cids+=("${svc_cids[@]}")
done
local -a cname
local cid name; for cid in "${cids[@]}"; do
name="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null)"; name="${name#/}"
[[ -n "$name" ]] && cname+=("$name")
done
local -a vols imgs
local img_id v
for cid in "${cids[@]}"; do
for v in ${(f)$(docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "$cid" 2>/dev/null)}; do
[[ -n "$v" ]] && vols+=("$v")
done
img_id="$(docker inspect -f '{{.Image}}' "$cid" 2>/dev/null)"
[[ -n "$img_id" ]] && imgs+=("$img_id")
done
local -a uniq_vols uniq_imgs
uniq_vols=(${(u)vols}); uniq_imgs=(${(u)imgs})
echo "πŸ—‘οΈ Removing containers..."; echo "──────────────────────────────"
if (( ${#cname[@]} )); then printf ' πŸ“¦ %s\n' "${cname[@]}"; else echo "No containers to remove."; fi
$dc rm -sfv "${services[@]}" >/dev/null 2>&1 || true
echo "──────────────────────────────"; echo "πŸ—‘οΈ Removing volumes..."; echo "──────────────────────────────"
if (( ${#uniq_vols[@]} )); then printf ' πŸ—„οΈ %s\n' "${uniq_vols[@]}"; docker volume rm -f "${uniq_vols[@]}" >/dev/null 2>&1 || true
else echo "No volumes to remove."; fi
echo "──────────────────────────────"; echo "πŸ—‘οΈ Removing images..."; echo "──────────────────────────────"
if (( ${#uniq_imgs[@]} )); then
local img ref short
for img in "${uniq_imgs[@]}"; do
short="${${img#sha256:}:0:12}"
ref="$(docker image inspect --format '{{if .RepoTags}}{{index .RepoTags 0}}{{else}}<untagged>{{end}}' "$img" 2>/dev/null)"
echo " πŸ–ΌοΈ ${short:-$img} ${ref}"
done
docker image rm -f "${uniq_imgs[@]}" >/dev/null 2>&1 || true
else echo "No images to remove."; fi
echo "πŸ”„ Rebuilding services..."; echo "──────────────────────────────"
echo "πŸ› οΈ Building Docker Compose services..."; echo "──────────────────────────────"
if $dc build "${services[@]}"; then echo "πŸŽ‰ All services built successfully!"; else echo "❌ One or more services failed to build."; fi
echo "πŸš€ Starting services..."; echo "──────────────────────────────"
$dc up -d "${services[@]}"; echo "[βœ“] Done."
}
ZPLUG
echo "βœ“ Plugin installed."
if ! grep -qE '^[[:space:]]*plugins=.*\bdclean\b' "$ZSHRC" 2>/dev/null; then
cat <<EOF
Add the plugin to your ~/.zshrc, then reload:
1) Edit plugins=(...) and include: dclean
Example:
plugins=(git dclean)
2) Reload:
source "$ZSHRC"
Done! Now run:
dclean <service> [service…]
EOF
else
echo "It looks like 'dclean' is already in plugins=(...). Reload with: source \"$ZSHRC\""
fi
;;
*)
echo "Unknown mode: $MODE"; exit 1;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment