Ошибка /bin/sh: not found чаще всего появляется при запуске контейнера Docker, выполнении shell-скрипта или сборке образа — и почти всегда означает, что система не может найти интерпретатор командной строки по указанному пути. Типичный сценарий: команда docker run завершается сообщением вроде exec /bin/sh: no such file or directory, хотя скрипт на месте и права выставлены.
Проблема обманчива: текст ошибки указывает на отсутствие sh, но реальная причина может быть совсем в другом — в невидимых символах переноса строки Windows, в образе без shell вообще или в битом симлинке. Ниже разберём, как быстро локализовать источник и исправить ситуацию без лишних действий.
Что означает ошибка /bin/sh: not found
Интерпретатор sh (Bourne Shell) — базовая оболочка Unix-подобных систем. Когда вы запускаете скрипт, указываете ENTRYPOINT в Dockerfile или выполняете команду через sh -c, система обращается к файлу /bin/sh. Если по этому пути ничего нет или файл не читается корректно, возникает рассматриваемая ошибка.
Важный нюанс: сообщение no such file or directory при запуске исполняемого файла может относиться не к самому скрипту, а к интерпретатору, указанному в его shebang (первая строка вида #!/bin/sh). Ядро читает эту строку и пытается найти интерпретатор — если путь «битый», ошибка выглядит так, будто отсутствует сам скрипт.
- 🐳 Docker-контейнеры — образ собран на базе scratch или distroless, где shell отсутствует в принципе
- 📄 Скрипты с CRLF — файл сохранён в Windows с окончаниями строк
\r\n, и система ищет интерпретатор/bin/sh\r - 🔗 Битый симлинк —
/bin/shявляется ссылкой на dash или bash, которые удалены или повреждены - 📦 Многоэтапная сборка — бинарник скопирован в минимальный образ, а команда запуска требует shell
Быстрая диагностика: что проверить в первую очередь
Прежде чем что-то менять, определите, где именно возникает сбой. Если ошибка появляется при запуске контейнера, посмотрите, на каком базовом образе он собран — команда docker inspect или просмотр Dockerfile даст ответ. Если сбой при выполнении локального скрипта — проверьте его первую строку и формат окончаний строк.
Полезная проверка для скрипта — вывести информацию о файле:
file script.sh
head -1 script.sh | od -c | head -2
Первая команда покажет тип файла и, что важно, упомянет CRLF line terminators, если они есть. Вторая выведет первую строку посимвольно — если после #!/bin/sh виден символ \r, причина найдена.
Причина 1: окончания строк CRLF в скрипте
Самая частая и самая коварная причина. Редакторы в Windows (включая некоторые настройки VS Code и Notepad++) сохраняют файлы с окончаниями строк \r\n. Git тоже может автоматически конвертировать окончания при клонировании, если включён параметр core.autocrlf. В результате shebang превращается в #!/bin/sh\r, и ядро ищет интерпретатор с «мусорным» символом в имени.
Исправить файл можно несколькими способами. Если установлена утилита dos2unix, достаточно одной команды:
dos2unix script.sh
Без неё подойдёт sed или tr:
sed -i 's/\r$//' script.sh
или
tr -d '\r' < script.sh > script_fixed.sh
⚠️ Внимание: после исправления окончаний строк проверьте, что у скрипта сохранились права на выполнение (chmod +x script.sh). Некоторые способы пересохранения файла сбрасывают флаг исполняемости.
Причина 2: в образе контейнера нет shell
Минималистичные базовые образы — scratch, distroless, некоторые сборки на базе alpine после очистки — могут вообще не содержать /bin/sh. Это осознанный выбор: меньше компонентов — меньше поверхность атаки и размер образа. Но если в Dockerfile указано ENTRYPOINT ["/bin/sh", "-c", "..."] или CMD в shell-форме, запуск завершится ошибкой.
Проверить наличие shell в образе можно, запустив контейнер с альтернативной точкой входа (если есть хоть какой-то бинарник) или просто изучив Dockerfile. Решений несколько:
- 🔧 Сменить базовый образ — перейти со scratch на alpine или debian-slim, где shell присутствует
- 📋 Использовать exec-форму — писать
ENTRYPOINT ["/app/mybinary"]без обёртки в shell, если бинарник самодостаточен - 🏗️ Скопировать shell вручную — в многоэтапной сборке перенести
/bin/shи его зависимости из build-стадии (хрупкий путь, требует учёта динамических библиотек) - 🔄 Переписать логику запуска — вынести shell-обвязку наружу, например в docker-compose или оркестратор
☑️ Проверка контейнера при ошибке /bin/sh
Причина 3: повреждён или удалён /bin/sh в системе
На обычной Linux-системе /bin/sh — это, как правило, символическая ссылка. В Debian и Ubuntu она указывает на dash, в некоторых дистрибутивах — на bash. Если целевой интерпретатор удалён, переустановлен с ошибкой или ссылка перезаписана вручную, любые скрипты с shebang #!/bin/sh перестанут работать.
Диагностика занимает пару команд. Посмотрите, куда ведёт ссылка и существует ли цель:
ls -l /bin/sh
readlink -f /bin/sh
Если цель отсутствует, восстановите её штатными средствами пакетного менеджера. В Debian-подобных системах пакет dash переустанавливается через apt, в Alpine — через apk. Точные названия пакетов зависят от дистрибутива, поэтому сверяйтесь с его документацией.
⚠️ Внимание: не перенаправляйте /bin/sh на произвольный интерпретатор «для удобства». Системные скрипты могут рассчитывать именно на POSIX-совместимое поведение dash, и замена способна нарушить загрузку сервисов и работу пакетного менеджера.
Почему /bin/sh — это не всегда bash
Исторически sh — оригинальная оболочка Bourne Shell. В современных дистрибутивах /bin/sh является ссылкой на минималистичный POSIX-совместимый интерпретатор: в Debian и Ubuntu это dash, во многих других системах — bash в режиме совместимости. Поэтому скрипты с bash-специфичными конструкциями (массивы, двойные квадратные скобки, некоторые расширения) могут ломаться, если запускать их через sh. Если скрипт написан под bash, указывайте в shebang именно #!/bin/bash или #!/usr/bin/env bash.
Ошибка при сборке образа: RUN и многоэтапные сборки
Отдельный сценарий — ошибка возникает не при запуске, а на этапе docker build, на инструкции RUN. Здесь причины те же, но проявляются иначе: либо базовый образ текущей стадии не содержит shell, либо в копируемом скрипте остались CRLF-окончания. Даже один скрипт с Windows-окончаниями строк способен остановить всю сборку на шаге RUN, причём сообщение об ошибке не всегда явно указывает на виновный файл.
Порядок действий при сбое сборки:
- Найдите в выводе сборки шаг, на котором произошла ошибка, и соответствующую инструкцию в Dockerfile.
- Проверьте формат строк всех скриптов, копируемых через
COPYдо этого шага. - Если используется многоэтапная сборка, убедитесь, что финальная стадия основана на образе с shell — либо уберите зависимость от shell.
- Пересоберите с флагом
--no-cache, чтобы исключить влияние закэшированных слоёв.
Сравнение типичных сценариев и решений
| Сценарий | Вероятная причина | Способ проверки | Решение |
|---|---|---|---|
| Запуск локального скрипта | CRLF в shebang | file script.sh |
dos2unix или sed |
| docker run завершается сразу | Образ без shell | Анализ Dockerfile, базового образа | Сменить образ или exec-форма ENTRYPOINT |
| Сбой на шаге RUN при build | CRLF в копируемом скрипте | Просмотр логов сборки | Исправить файл, пересобрать с --no-cache |
| Скрипты не работают на хосте | Битый симлинк /bin/sh | ls -l /bin/sh |
Переустановить dash/bash |
| Ошибка только в CI/CD | Git конвертирует EOL при checkout | Проверить .gitattributes и core.autocrlf | Настроить .gitattributes с eol=lf |
Профилактика: как не столкнуться с ошибкой снова
Большинство повторных случаев связано с рассинхронизацией окружений: разработчик работает в Windows, CI собирает в Linux, а контейнер запускается на минимальном образе. Чтобы свести риски к минимуму, вам стоит закрепить несколько привычек на уровне проекта, а не личных настроек редактора.
Настройте репозиторий так, чтобы формат файлов контролировался автоматически: .gitattributes с принудительным eol=lf для скриптов и конфигов, линтеры вроде shellcheck в пайплайне, проверка Dockerfile на соответствие базового образа и команд запуска. Необходимо также явно документировать, какие образы команда использует как базовые и есть ли в них shell.
Часто задаваемые вопросы
Почему скрипт запускается через sh script.sh, но падает при ./script.sh?
При вызове sh script.sh вы явно указываете интерпретатор, и shebang игнорируется. При запуске через ./script.sh ядро читает первую строку файла — и если там #!/bin/sh с символом возврата каретки или путь к отсутствующему интерпретатору, возникает ошибка. Расхождение поведения — верный признак проблемы именно в shebang.
Можно ли запустить shell внутри контейнера, собранного на scratch?
Нет, если в образе физически нет бинарника shell и его зависимостей. Команда docker exec -it container sh завершится той же ошибкой. Варианты: пересобрать образ с shell, использовать отладочные ephemeral-контейнеры в Kubernetes или анализировать образ инструментами вроде docker export и просмотра файловой системы с хоста.
Чем alpine отличается от debian-slim в контексте этой ошибки?
В alpine shell есть (/bin/sh — ссылка на busybox), но система использует библиотеку musl вместо glibc, поэтому бинарники, собранные под glibc, могут не запускаться с похожей ошибкой «not found». В debian-slim используется glibc и полноценный dash. Если бинарник «не находится» в alpine, хотя файл на месте, проверьте его зависимости командой ldd.
Git сам портит окончания строк. Как это отключить?
Поведение управляется параметром core.autocrlf и файлом .gitattributes. Надёжнее всего не полагаться на локальные настройки, а зафиксировать правила в репозитории: добавьте в .gitattributes строку *.sh text eol=lf. После этого Git будет хранить и выгружать скрипты с Unix-окончаниями независимо от настроек машины разработчика.
Ошибка появляется только в CI-пайплайне, локально всё работает. Что проверить?
Начните с окружения CI: какой runner-образ используется, есть ли в нём shell, как настроен checkout кода (некоторые CI-системы применяют собственные правила конвертации EOL). Сравните формат файла локально и в CI командой file прямо в шаге пайплайна — это быстрее всего покажет расхождение.