Свежая виртуалка или контейнер, вход по ssh, первый же запуск любого скрипта на perl выдаёт четыре строки предупреждения "perl: warning: Setting locale failed". Привычный сценарий для многих администраторов: ворчат apt, man показывает сообщения на английском, скрипты сортировки строк с кириллицей ведут себя неожиданно, а логи пестрят тревожными runtime warnings. Голая причина проста: системная локаль объявлена, а сгенерировать её забыли. Ниже разбирается практическая диагностика через locale и locale -a, ручная генерация ru_RU.UTF-8, фиксация постоянной локали в /etc/default/locale, тонкости LC_ALL в сравнении с LANG и LC_CTYPE, а также надёжный план, чтобы предупреждение не возвращалось снова.

Что на самом деле означает предупреждение perl о locale

Предупреждение приходит не от самого perl, а из libc: интерпретатор при старте вызывает setlocale() со значениями из переменных окружения LANG, LC_ALL и LC_CTYPE, а библиотека не находит скомпилированные файлы локали в /usr/lib/locale или /usr/share/locale. Тогда в stderr падает типовой текст:

perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
	LANGUAGE = (unset),
	LC_ALL = (unset),
	LC_CTYPE = "ru_RU.UTF-8",
	LANG = "ru_RU.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").

Первая строка фиксирует сам факт отказа. Вторая группа строк с табуляцией перечисляет текущие значения переменных: здесь видно, что LANG и LC_CTYPE выставлены в ru_RU.UTF-8, а LC_ALL и LANGUAGE пусты. Ключевая фраза стоит в конце: "Falling back to the standard locale (C)". Программа не падает, она продолжает работу в минимальной POSIX локали, но уже без корректной поддержки многобайтовых символов, денежных форматов и правильной сортировки кириллицы.

Почему это происходит именно на свежих серверах и в Docker образах? Потому что переменные окружения часто прилетают извне: ssh клиент на рабочем ноутбуке передаёт свои LANG и LC_* через механизм AcceptEnv в sshd_config, а на сервере такая локаль попросту не сгенерирована. В минимальных образах типа debian:12 или ubuntu:24.04 пакет locales вырезан ради объёма, и в /usr/share/locale пусто. Результат тот же самый: локаль объявлена, файлов для неё нет, предупреждение появляется снова и снова в каждом cron письме и каждом вызове apt.

Диагностика текущего состояния через locale и locale -a

Первый шаг в живой диагностике это команда locale без аргументов. Она показывает эффективные значения всех категорий локали для текущего шелла:

locale

Типовой вывод на сервере с проблемой выглядит так:

LANG=ru_RU.UTF-8
LANGUAGE=
LC_CTYPE="ru_RU.UTF-8"
LC_NUMERIC="ru_RU.UTF-8"
LC_TIME="ru_RU.UTF-8"
LC_COLLATE="ru_RU.UTF-8"
LC_MONETARY="ru_RU.UTF-8"
LC_MESSAGES="ru_RU.UTF-8"
LC_PAPER="ru_RU.UTF-8"
LC_NAME="ru_RU.UTF-8"
LC_ADDRESS="ru_RU.UTF-8"
LC_TELEPHONE="ru_RU.UTF-8"
LC_MEASUREMENT="ru_RU.UTF-8"
LC_IDENTIFICATION="ru_RU.UTF-8"
LC_ALL=
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory

Двенадцать строк с категориями говорят, что шелл и perl хотят ru_RU.UTF-8, а три строки ошибок "No such file or directory" подтверждают: файлов локали в системе нет. Дальше решает команда locale -a, она перечисляет все локали, которые реально скомпилированы:

locale -a

На голом сервере ответ оказывается коротким:

C
C.utf8
POSIX

Русской локали среди трёх строк нет, вот корень проблемы. Кстати, регистр и синтаксис имён различаются: locale -a вывела бы готовую локаль как ru_RU.utf8 со строчным суффиксом, тогда как в LANG обычно пишут ru_RU.UTF-8, и система их связывает нормально. Для точного поиска удобно применить фильтр:

locale -a | grep -i ru_ru

Пустой вывод означает, что локали с префиксом ru_RU нет вообще. Если вместо этого появляется строка ru_RU.utf8, локаль сгенерирована, и причину надо искать в некорректном написании имени в переменных окружения или в устаревшем кеше. Ещё одна полезная проверка это физическое наличие каталога:

ls /usr/lib/locale/locale-archive 2>/dev/null; ls /usr/share/locale/ru/LC_MESSAGES 2>/dev/null | head -3

Архив /usr/lib/locale/locale-archive содержит скомпилированные локали в бинарном виде, его отсутствие либо размер в пары килобайт говорит о том, что locale-gen ещё ни разу не отрабатывал. Диагностика на этом завершается: установленный диагноз "локаль объявлена, но не сгенерирована".

Отдельного упоминания заслуживает проверка окружения внутри уже запущенного процесса, когда чужой сервис внезапно ругается на локаль, а интерактивный шелл чист. Достаточно заглянуть в /proc:

sudo tr '\0' '\n' < /proc/$(pgrep -f myscript.pl | head -1)/environ | grep -E '^(LANG|LC_)'

Здесь tr заменяет нулевые байты на переводы строк, потому что файл environ хранит переменные через символ NUL, а grep фильтрует только переменные локали. Если процесс стартовал до правки /etc/default/locale, его окружение зафиксировано старым значением, и помогает только перезапуск. Ещё быстрый способ убедиться в том, что конкретная локаль работает, это вызов утилиты с явной локалью:

LC_ALL=ru_RU.UTF-8 date +%B

Если команда выводит название месяца по-русски, например "октябрь", локаль сгенерирована и функционирует; в противном случае вернётся знакомое предупреждение.

Генерация ru_RU.UTF-8 через locale-gen и правка /etc/locale.gen

Лечение начинается с файла /etc/locale.gen, в котором перечислены локали для генерации, по одной на строку. Свежая установка выглядит примерно так: десятки строк закомментированы символом "#", и среди них есть нужная:

# ru_RU.UTF-8 UTF-8
# ru_RU.KOI8-R KOI8-R
# ru_RU ISO-8859-5

Раскомментировать строку проще всего одной командой sed, без открытия редактора по ssh с 200 ms задержкой:

sudo sed -i 's/^# *ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen

Опция -i вносит правку прямо в файл, а регулярное выражение учитывает возможный пробел после символа комментария. Дальше запускается сама генерация:

sudo locale-gen ru_RU.UTF-8

Вывод подтверждает компиляцию:

Generating locales (this might take a while)...
  ru_RU.UTF-8... done
Generation complete.

На типовой виртуалке с двумя vCPU и SSD это занимает две-четыре секунды на локаль. Повторная проверка через locale -a | grep -i ru_ru теперь выдаёт ru_RU.utf8, что фиксирует успех. В системах без полноценного пакета locales сначала требуется его установка: sudo apt install -y locales. В минимальных Docker образах удобен однострочник в Dockerfile:

# Устанавливаем пакет локалей и сразу генерируем русскую
RUN apt-get update && apt-get install -y locales \
    && echo "ru_RU.UTF-8 UTF-8" > /etc/locale.gen \
    && locale-gen ru_RU.UTF-8

Такой подход добавляет образу примерно 10-15 МБ, зато избавляет от предупреждений на всех этапах: сборке, CI и всякий раз при docker exec внутрь работающего контейнера.

Фиксация постоянной локали через /etc/default/locale и update-locale

Генерация даёт файлы локали, но постоянное значение переменных для всех сессий, сервисов и cron задач хранится отдельно. В Debian и Ubuntu этим занимается файл /etc/default/locale, а обновляет его утилита update-locale:

sudo update-locale LANG=ru_RU.UTF-8

После выполнения файл приобретает лаконичный вид:

LANG=ru_RU.UTF-8

Утилита update-locale умеет и больше: с ключом LC_MESSAGES=en_US.UTF-8 можно оставить интерфейсные сообщения на английском, а формат дат и чисел задать русским, что многим администраторам удобнее при чтении логов и гуглении ошибок. После правки изменения подхватываются в новых login сессиях через pam_env, текущий шелл править надо вручную командой export или перелогином. Проверка проста:

cat /etc/default/locale

Для systemd сервисов обычный /etc/default/locale учитывается не всегда, потому что unit файлы запускаются в минимальном окружении. Надёжный приём это добавление директивы Environment="LANG=ru_RU.UTF-8" в секцию [Service] либо глобальный drop-in:

[Service]
Environment="LANG=ru_RU.UTF-8"
Environment="LC_ALL=ru_RU.UTF-8"

После правки unit файла требуется sudo systemctl daemon-reload и перезапуск сервиса. Cron наследует окружение из crontab, поэтому при необходимости строки LANG=ru_RU.UTF-8 можно дописать в начало crontab -e, и это избавит почтовых ящиков администратора от сотен писем с предупреждением perl за ночь.

Разница между LC_ALL LANG и LC_CTYPE на практике

Иерархия переменных устроена тремя уровнями, и путаница между ними рождает половину проблем с локалью. LANG задаёт значение по умолчанию для всех двенадцати категорий сразу. Отдельные переменные вида LC_TIME или LC_CTYPE перекрывают LANG для своей категории. LC_ALL стоит выше всех и насильно перекрывает всё, при этом она предназначена для временного разового использования, а не для постоянной настройки.

Практический пример, когда смешанная локаль оправдана: администратор хочет русские правила сортировки и обработки символов, но английские сообщения об ошибках для поиска решений в интернете. Тогда выставляется LANG=ru_RU.UTF-8 плюс LC_MESSAGES=en_US.UTF-8, и обе локали должны быть сгенерированы через locale-gen, иначе предупреждение вернётся. Проверить, что реально выставлено в процессе, можно так:

env | grep -E '^(LANG|LC_)' 

Типичная ловушка с LC_ALL выглядит так: кто-то прописывает в /etc/default/locale строку LC_ALL=ru_RU.UTF-8 "чтобы наверняка", а спустя месяц администратор вводит временную команду LC_TIME=POSIX для скрипта и не понимает, почему она не действует. Ответ прост: LC_ALL сидит выше и глушит всё остальное. Правильное применение LC_ALL это префикс перед одной командой, например LC_ALL=C sort file.txt, когда требуется байтовая сортировка без учёта юникодных правил и на порядок быстрее: на файле в 10 млн строк сортировка в локали C может отработать примерно в полтора-два раза быстрее, чем в UTF-8.

Типовые причины повторного появления ошибки и профилактика

Когда локаль сгенерирована и /etc/default/locale заполнен, предупреждение всё же иногда возвращается, и причины обычно сводятся к компактному списку:

  1. ssh клиент передаёт локаль ноутбука через SendEnv LANG LC_*, а sshd на сервере принимает её по AcceptEnv; если на сервере нет специфичной локали клиента, например en_US.UTF-8 вместо ru_RU.UTF-8, предупреждение всплывает; профилактика: сгенерировать обе локали либо убрать AcceptEnv из /etc/ssh/sshd_config и задать серверную локаль принудительно;
  2. контейнер собран из минимального базового образа без пакета locales; профилактика: строка генерации в Dockerfile, как показано выше, либо переменная LANG=C.UTF-8, которая присутствует в большинстве базовых образов без генерации;
  3. приложение читает собственный конфиг с жёстко прописанной устаревшей локалью типа ru_RU.CP1251; профилактика: перевести конфиги на UTF-8, символьная таблица KOI8-R и однобайтовые кодировки выводятся из эксплуатации;
  4. cron задачи выполняются от имени пользователя с чужим домашним каталогом или с устаревшим ~/.profile, где export LC_ALL указывает на несуществующую локаль.

Полезна и профилактическая команда для регулярного аудита серверов:

locale -a | wc -l; locale; grep -v '^#' /etc/locale.gen

Три команды подряд показывают количество скомпилированных локалей, текущие значения переменных и список разрешённых к генерации. Если число локалей равно трём (C, C.utf8, POSIX), а /etc/default/locale требует ru_RU.UTF-8, сервер ждёт очередной визит в почту от cron с предупреждением perl. Зафиксировав /etc/locale.gen, выполнив locale-gen и update-locale LANG=ru_RU.UTF-8, администратор получает систему, где perl, man, sort, awk и все прикладные скрипты видят корректную русскую локаль, а логи остаются чистыми. Десять минут методичной диагностики заменяют часы вычистки ночных алертов и разговоров с командой об "ещё одной мелкой баге".