#!/usr/bin/env bash set -euo pipefail # Docker Compose cannot reuse a fixed `container_name` that is already owned by # another compose project. Local development often leaves those containers behind # after moving or renaming the repo, so remove only compose-owned stale conflicts # before `docker compose up` reaches Docker's generic name-conflict failure. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" cd "${PROJECT_ROOT}" if ! command -v python3 >/dev/null 2>&1; then printf 'python3 is required to inspect docker compose config before startup\n' >&2 exit 1 fi compose_config="$(docker compose config --format json)" compose_project="$(printf '%s' "${compose_config}" | python3 -c 'import json, sys; print(json.load(sys.stdin).get("name", ""))')" if [[ -z "${compose_project}" ]]; then printf 'docker compose project name is empty; cannot resolve container conflicts safely\n' >&2 exit 1 fi spec_file="$(mktemp)" trap 'rm -f "${spec_file}"' EXIT printf '%s' "${compose_config}" | python3 -c 'import json, sys config = json.load(sys.stdin) for service_name, service in sorted(config.get("services", {}).items()): container_name = service.get("container_name") if container_name: print(f"{service_name}\t{container_name}") ' > "${spec_file}" for requested_service in "$@"; do if ! awk -F '\t' -v service="${requested_service}" '$1 == service { found = 1 } END { exit !found }' "${spec_file}"; then printf 'unknown compose service for conflict resolution: %s\n' "${requested_service}" >&2 exit 1 fi done docker_label() { local container_name="$1" local label_name="$2" docker inspect --format "{{ with index .Config.Labels \"${label_name}\" }}{{ . }}{{ end }}" "${container_name}" 2>/dev/null || true } requested_services=" $* " while IFS=$'\t' read -r service_name container_name; do if [[ -z "${service_name}" || -z "${container_name}" ]]; then continue fi if [[ "$#" -gt 0 && "${requested_services}" != *" ${service_name} "* ]]; then continue fi container_id="$(docker inspect --format '{{.Id}}' "${container_name}" 2>/dev/null || true)" if [[ -z "${container_id}" ]]; then continue fi label_project="$(docker_label "${container_name}" "com.docker.compose.project")" label_service="$(docker_label "${container_name}" "com.docker.compose.service")" label_workdir="$(docker_label "${container_name}" "com.docker.compose.project.working_dir")" label_config_files="$(docker_label "${container_name}" "com.docker.compose.project.config_files")" # A container created by this exact compose project can be reused by compose; # deleting it would discard useful local process state without fixing anything. if [[ "${label_project}" == "${compose_project}" ]] && \ { [[ "${label_workdir}" == "${PROJECT_ROOT}" ]] || [[ "${label_config_files}" == *"${PROJECT_ROOT}/"* ]]; }; then continue fi # Manual or unlabeled containers have unknown ownership. Fail with a precise # command instead of guessing that this repo is allowed to remove them. if [[ -z "${label_project}" || -z "${label_service}" ]]; then printf 'container name conflict: %s (%s) is not managed by Docker Compose\n' "${container_name}" "${container_id:0:12}" >&2 printf 'remove or rename it before startup, for example: docker rm -f %s\n' "${container_name}" >&2 exit 1 fi printf 'removing stale compose container: %s (%s, project=%s, service=%s, working_dir=%s)\n' \ "${container_name}" "${container_id:0:12}" "${label_project}" "${label_service}" "${label_workdir:-unknown}" docker rm -f "${container_name}" >/dev/null done < "${spec_file}"