Переменные окружения
Переменные окружения
Это пары «имя=значение», которые операционная система передаёт каждому запускаемому процессу. Через них программа узнаёт свои настройки и секреты, не зашитые в код.
История
Переменные окружения родились в Bell Labs в 1979 году, в седьмой версии Unix (Unix V7). Автор — Стивен Борн (Stephen Bourne), тот самый, чьей фамилией назван sh (Bourne shell). До этого, в Unix V6 (1975), у программ был только массив аргументов командной строки — argv. Этого хватало, пока программы были маленькими, но как только понадобилось передавать сразу десяток параметров (где живёт почта, какой язык, где терминал), стало очевидно: командная строка превращается в кашу из флагов.
Борн добавил в системный вызов execve() третий аргумент — envp (environment pointer). Это NULL-завершённый массив строк вида "KEY=VALUE". Идея простая: пусть каждая новая программа получает не только аргументы, но и «вторую руку» — набор именованных параметров, которые она может читать или игнорировать.
Дальше — этапы канонизации:
- 1988, POSIX.1 (IEEE Std 1003.1) — переменные окружения становятся официальным стандартом. Появляется обязательный набор имён:
HOME,PATH,LANG,TZ,USER,SHELL. - 2011, Adam Wiggins (соучредитель Heroku) публикует манифест 12-factor app — двенадцать правил для облачных приложений. Третий фактор — «Конфигурация в окружении». Это закрепило переменные окружения как канонический способ конфигурировать веб-приложения.
- 2012, Брэндон Кииперс выпускает Ruby-гем
dotenv— он читает файл.envи подгружает переменные в окружение процесса. Идея разошлась по всем языкам:python-dotenv,dotenvв Node.js, аналоги в Go, Rust, PHP. - 2015–2020-е, эпоха контейнеров: Docker, Kubernetes, AWS Lambda, Vercel, Fly.io — все они дают конфигурировать приложения через переменные окружения как стандартный интерфейс.
Сейчас, в 2026-м, это базовый словарь любого инженера: фраза «прокинь это через env» понятна и Go-программисту, и тому, кто настраивает GitHub Actions, и DevOps на Kubernetes.
Что это такое
Переменные окружения — это таблица «имя → значение», которую операционная система прикладывает к каждому процессу. Имена — строки латиницей в верхнем регистре по соглашению (DATABASE_URL, TELEGRAM_TOKEN), значения — произвольные строки. Просто строки, не числа, не словари, не списки — только текст. Это важно: в окружении нет типов.
Каждый процесс получает свою копию окружения при запуске. Когда программа запускает другую программу (через fork+exec в Unix), дочерний процесс наследует окружение родителя — но это именно копия. Если ребёнок изменит у себя PATH, родитель об этом никогда не узнает. Это одностороннее наследование, как сундук с приданым: что положили — то и взял с собой, обратно ничего не возвращается.
В системном смысле окружение живёт в памяти процесса. В C — это глобальная переменная extern char **environ, в Python — os.environ (словарь, имитирующий это поведение), в shell — то, что показывает команда env или printenv. На Linux окружение работающего процесса можно подсмотреть через файл /proc/<PID>/environ (если хватает прав). На macOS аналогичного публичного файла нет, но через ps -E можно увидеть env-переменные своих процессов.
Чем переменные окружения отличаются от похожего:
- vs аргументы командной строки (
argv). Аргументы — это явный, видимый «фасад» (его покажетps). Окружение — спрятано в недрах процесса, не светится в списках. Поэтому секреты передают через env, а не через флаги. - vs файл конфигурации. Файл живёт на диске, его можно читать и редактировать снаружи; env живёт только в памяти процесса и исчезает с ним. Файл удобен для сложной структуры, env — для плоского ключ-значение.
- vs shell-переменные. В shell бывают два типа переменных: локальные (только внутри текущего shell) и экспортированные в окружение (через
export). Локальные не передаются дочерним процессам. Это частая путаница новичков: «я же написалVAR=hello, почему программа не видит?»
Аналогии из жизни
1. Рюкзак, который мама собрала в школу.
Утром мама положила в рюкзак тетради, бутерброд, ключи. В школе ты можешь достать оттуда что угодно, можешь даже что-то добавить. Когда возвращаешься домой — мама не узнает, что ты подложил себе фантики; её рюкзак (то, что осталось у неё) не меняется.
Где ломается: в реальности рюкзак физически один и переходит из рук в руки. В случае env — рюкзак копируется на пороге школы: мама и ты держите два разных одинаковых рюкзака. Менять можно только свой.
2. Доска объявлений в подъезде.
Управляющая компания вывесила объявления: режим работы лифта, телефон диспетчерской, тариф за воду. Любой жилец, выходя из квартиры, может прочитать.
Где ломается: доска одна на всех, видна всем. Env — это доска только для одной квартиры (процесса) и её детей. Соседский ребёнок (другой процесс) её не видит. И в отличие от настоящей доски, переменные окружения у каждого жильца свои, не общие.
3. Конверт с инструкциями от заказчика курьеру.
Курьеру выдают конверт: куда ехать, что забрать, какой код от подъезда. Курьер берёт его с собой, в дороге читает. Когда курьер передаёт заказ субподрядчику — он копирует конверт (или передаёт оригинал — но в случае env всегда копию).
Где ломается: в реальности заказчик может позвонить и поменять адрес. С env так нельзя: пока процесс запущен, родитель в его окружение не дотянется снаружи. Хочешь поменять — перезапусти.
Как это работает
Системный вызов execve(path, argv, envp) — главная точка входа в Unix. У него три аргумента:
path— путь к исполняемому файлу.argv— массив аргументов («имя программы», «аргумент 1», ..., NULL).envp— массив окружения («KEY1=VALUE1», «KEY2=VALUE2», ..., NULL).
Когда ты в терминале набираешь python myscript.py --verbose:
- shell делает
fork()— создаёт копию себя. - В копии вызывает
execve("/usr/bin/python", ["python", "myscript.py", "--verbose"], envp), гдеenvp— это всё, что у shell было вexport. - Ядро заменяет память процесса на образ python, копируя туда
argvиenvp. - Python запускается, читает
os.environ— это и есть распарсенныйenvp.
В коде на C это выглядит так:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *db = getenv("DATABASE_URL");
if (!db) { fprintf(stderr, "DATABASE_URL not set\n"); return 1; }
printf("connecting to %s\n", db);
return 0;
}
В Python — то же самое одной строкой:
import os
db = os.environ["DATABASE_URL"] # упадёт, если не задано
db = os.environ.get("DATABASE_URL", "sqlite:///fallback.db") # с дефолтом
В shell:
echo $DATABASE_URL
echo "${DATABASE_URL:-sqlite:///fallback.db}" # с дефолтом
Жизненный цикл переменной от загрузки системы до запуска программы:
- Ядро запускает первый процесс (
init/launchd/systemd) с минимальным окружением — буквально пара переменных:PATH=/bin:/usr/bin, иногдаHOME. - Менеджер сессии при логине пользователя выставляет
USER,HOME,SHELL, читает/etc/environment(на Linux) илиlaunchd.conf(на старых macOS). - Shell при старте читает свои конфиги:
~/.profile,~/.bashrc,~/.zshenv,~/.zshrc. В них живут командыexport VAR=value, добавляющие переменные в окружение текущего shell. - Запуск программы из shell — программа наследует окружение shell.
- Программа может прочитать окружение через
getenv()/os.environ, может добавить свои черезsetenv()/os.environ[...]=.... Эти добавления уйдут в её детей, но не вернутся к родителю.
Ключевая контринтуитивность
Окружение — это снимок, сделанный в момент запуска. Если ты поменял ~/.zshenv и не перезапустил процесс — он работает со старыми значениями. Любой systemd-сервис, launchd-агент, докер-контейнер должен быть перезапущен, чтобы увидеть новые env.
Команды важные на каждый день:
env— показать всё окружение текущего shell.printenv VAR— значение одной переменной.export VAR=value— выставить переменную и сделать её видимой детям.unset VAR— удалить.VAR=value command— выставить только на один вызов.env -i command— запустить с пустым окружением (полезно для проверки скриптов).
Где встречается в обычной жизни
- Когда у тебя на сайте интерфейс на русском, а у друга на английском — на сервере одно приложение, но клиенту отвечают на разных языках. Часто это решается переменной
LANGилиLC_ALLв контейнере приложения; для тебя сервер запустили сLANG=ru_RU.UTF-8. - Когда время на компьютере показывается правильно — переменная
TZ=Europe/Moscowговорит программам, как переводить UTC в локальное. Если её нет, программа смотрит файл системного часового пояса. - Когда
cdсразу попадает в твою домашнюю папку — это$HOME. Без неёcdбез аргументов не знает, куда идти. - Когда команда
gitнаходится без указания полного пути — это$PATH. Shell перебирает папки изPATHпо порядку и берёт первое совпадение. - Когда цвета в терминале правильные —
$TERMсообщает, какой эмулятор терминала (xterm-256color, screen), и приложения подбирают escape-последовательности.
Где встречается в IT и бизнесе
- Хранение секретов: API-токены, пароли БД, ключи шифрования. Канонический пример из 12-factor:
DATABASE_URL=postgres://user:pass@host/db. Секрет не лежит в репозитории, а подкладывается окружением. - Переключение dev/staging/prod: одна и та же сборка приложения работает в трёх окружениях, потому что
NODE_ENV=productionвключает минификацию и кэширование,NODE_ENV=development— наоборот. - Конфигурация контейнеров: Docker —
-e DATABASE_URL=..., Compose —environment:вdocker-compose.yml, Kubernetes —env:в спецификации пода, AWS Lambda — поле Environment Variables в консоли. - CI/CD пайплайны: GitHub Actions, GitLab CI, CircleCI — все они хранят секреты у себя и пробрасывают их в job через env:
${{ secrets.NPM_TOKEN }}превращается в переменную окружения процесса сборки. - Feature flags на старте процесса:
ENABLE_NEW_DASHBOARD=true— приложение при запуске читает и решает, показать новый дашборд или старый. Простейшая версия фичефлагов до того, как поставить отдельный сервис типа LaunchDarkly.
Кто пользуется
Если коротко — все. Но есть знаковые применения:
- Heroku (с 2007) — первая большая платформа, где «конфиг = env-переменные» было единственным каналом. Команда
heroku config:set DATABASE_URL=...стала эталоном. - Docker (с 2013) — флаг
-eили--env-fileесть в каждом туториале. По разным оценкам, на 2024 год в продакшене работает порядка 8 миллионов докер-контейнеров одномоментно, и почти все они конфигурируются через env. - Kubernetes — поля
envиenvFromв спецификации пода. Большие компании прокидывают сотни переменных через ConfigMap и Secret. - AWS Lambda, Google Cloud Functions, Cloudflare Workers — серверлесс-функции конфигурируются исключительно через env (плюс secret-менеджеры, которые в итоге всё равно превращаются в env).
- CI-системы: GitHub Actions — 100+ миллионов репозиториев на GitHub, и почти каждый со своим набором env-секретов в Actions.
- Я.Облако, Selectel, VK Cloud — российские облака, у каждого свой UI для секретов, но на уровне приложения это всё равно
os.environ.
Альтернативы и конкуренты
Файлы конфигурации (YAML/TOML/JSON/INI).
+ Структурированные данные: вложенные объекты, списки, типы.
+ Можно комментировать, версионировать в git (если без секретов).
− Секреты в файлах — антипаттерн (попадают в git, бэкапы, образы).
− Нужно решать, где файл лежит, кто его кладёт.
CLI-флаги (--db-url=...).
+ Явно и видно при запуске.
+ Хорошо для разовых вызовов CLI-утилит.
− Видны в ps aux любому пользователю на машине — категорически плохо для секретов.
− Сложно передавать много значений (флагов десятки).
Vault / AWS Secrets Manager / HashiCorp Vault / Doppler.
+ Централизованное хранилище, ротация, аудит доступа.
+ Можно дать сервису короткоживущий токен вместо вечного пароля.
− Дополнительная зависимость: если Vault недоступен — приложение не стартует.
− Сложность настройки, цена.
Аргументы конструктора / DI-контейнер.
+ Типы, IDE-автодополнение, проверки на этапе компиляции.
+ Легко тестировать (передаёшь mock в конструктор).
− Где-то на самом верху всё равно нужно прочитать значения из мира — обычно из env.
На практике это не «или–или»
Современная архитектура — слоёная: секреты в Vault, оттуда подтягиваются в env при старте, приложение читает os.environ и собирает оттуда объект конфигурации с типами через Pydantic/zod/joi. Env здесь — клей, а не единственная защита.
Когда НЕ стоит использовать
- Когда конфигурация структурированная: вложенные объекты, списки серверов, схема валидации. Env-переменная — это плоская строка. Запихнуть туда JSON-сериализованный список можно, но это уродство; лучше YAML-файл.
- Когда нужна горячая перезагрузка без рестарта: env читается один раз при старте. Хочешь менять без рестарта — кладёшь в файл или внешний сервис конфигурации (Consul, etcd, Firebase Remote Config).
- Когда секрет действительно секретен от других процессов на той же машине: на Linux любой процесс того же пользователя через
/proc/<PID>/environпрочитает чужие переменные. Если угроза включает «локальный пользователь с тем же UID» — нужен честный secret manager с короткоживущими токенами.
Связанные понятия
- 12-factor app — манифест из двенадцати правил для облачных приложений; третий — «Config in environment».
- dotenv (
.envфайл) — текстовый файл со строкамиKEY=VALUE, который библиотека-загрузчик подсасывает в окружение процесса. - export / setenv / putenv — команды и функции, добавляющие переменную в окружение текущего процесса.
/proc/<PID>/environ— псевдофайл Linux, через который можно посмотреть окружение работающего процесса.- shell variables vs environment variables — в shell различают локальные (только в этом shell) и экспортированные (видны детям). Это разные сущности.
- HashiCorp Vault — внешнее хранилище секретов, через которое часто подкладывают env-переменные при старте приложения.
Литература и источники
- Kernighan & Pike. The UNIX Programming Environment, 1984 (en). Классика — там же объясняется, как Unix-программа разговаривает с окружением.
- 12factor.net — манифест 12-factor app целиком, страница «III. Config».
- man 7 environ — короткая, точная справка по переменным окружения в Linux (
man environв терминале). - POSIX.1-2008 (IEEE Std 1003.1), раздел 8.1 «Environment Variables» — формальная спецификация. Доступна на сайте Open Group.
- Wikipedia: «Environment variable» —
en.wikipedia.org/wiki/Environment_variable, есть и русская версия, но английская содержательнее. - Документация python-dotenv на pypi.org — как
.env-файлы работают на практике в Python; полезно посмотреть, чтобы понять, что dotenv это всего лишь парсер текстового файла, а не магия.
Где встретилось у меня
Вчера делал аудит секретов в дереве проектов: разные локальные агенты складывали API-токены в свои .env-файлы, и эти файлы синхронизировались через облачное хранилище — то есть пароли уезжали в облако вместе с обычными настройками. Унифицировал хранение секретов в одном месте вне облака (~/.zshenv), а .env агентов оставил пустыми или только с публичными параметрами. Понял, что приоритет источников у каждого агента свой — некоторые читают os.environ поверх .env, и это спасает.
Краткое резюме
- Переменные окружения — это плоская таблица «имя=значение», копия которой передаётся каждому новому процессу.
- Унаследованы от Unix V7 (1979), канонизированы POSIX, стали стандартом облачной конфигурации благодаря 12-factor app.
- Главная сила — простота и универсальность: одинаково работают в bash, Docker, Kubernetes, AWS Lambda и CI.
- Главная слабость — плоские строки и снимок на момент старта: не подходят для сложной конфигурации и горячей перезагрузки.
- Канал передачи секретов — но не сам по себе secret manager: на одной машине процессы того же пользователя видят чужие env через
/proc/<PID>/environ. - В реальной архитектуре env — клей между настоящим хранилищем секретов и кодом, который читает
os.environ.