Недавно я впервые занялся деплоем проектов и столкнулся с проблемой: как аккуратно подгружать переменные окружения для бэкенда и фронтенда.
На словах задача простая: подключи .env — и готово. Но на практике всё сложнее:
Вручную забивать ключи в админку (Coolify, Vercel, Heroku) неудобно. Каждый раз открывать интерфейс, копипастить значения, следить, чтобы они совпадали между окружениями… Короче, боль.
Права доступа обычно бинарные: либо у человека полный доступ ко всем переменным, либо никакого. Ограничить точечно нельзя.
Дублирование. В dev, staging и prod приходится руками синхронизировать одни и те же ключи. Ошибки неминуемы.
Нет истории изменений. Кто-то обновил переменную — и всё, старое значение потеряно.
Нет нормальной интеграции с пайплайном. Ключи можно скопировать в CI/CD, но это опять чужая админка и ручная работа.
Хочется чего-то системного:
все секреты хранятся в одном месте;
доступ можно раздавать точечно (одним — только dev, другим — только prod);
ключи автоматически подтягиваются при деплое.
Так я пришёл к Bitwarden Secrets Manager и уткнулся в детали, которых не нашёл ни в документации, ни на реддите. В этом посте делюсь готовым решением: как подружить Bitwarden CLI с деплоем через Docker.
Bitwarden Secrets Manager — это сервис, через который можно централизованно хранить, управлять и развертывать ключи в больших проектах.
Исходники
Бэкенд — NestJS.
Фронтенд — Vite.
Оба деплоятся через Docker, управляются через self-hosted Coolify.
Coolify — это open-source PaaS (аналог Heroku, Render, Railway). Запускает контейнеры, следит за логами, хранит ключи, делает деплой из гита. Маленькая облачная платформа на своём сервере.
Bitwarden Secrets Manager и CLI
У Bitwarden есть два полезных инструмента:
Secrets Manager — сервис для централизованного хранения ключей. Разные права доступа можно выдавать людям, сервисам или пайплайнам.
Bitwarden CLI (bws) — консольная утилита, которая умеет логиниться, доставать ключи из secrets manager и подставлять их куда угодно.
Комбо идеально подходит для CI/CD: все ключи хранятся в одном месте, а при деплое вы их автоматически подтягиваете.
Настраиваем Bitwarden
В Secrets Manager создаём два проекта:
dev — для переменных дев окружения,
prod — для переменных прода.
Чаще фронтенд и бэкенд разделяют на отдельные проекты. Здесь же для наглядности используем два: принцип работы не меняется, меняется уровень детализации.
Внутри каждого добавляем свои ключи:
prod.DATABASE_URL
prod.VITE_API_URL
dev.DATABASE_URL
dev.VITE_API_URL
В итоге получаем централизованное и понятное хранилище переменных.
Machine Account
Чтобы сервер мог сам подтягивать переменные, Bitwarden предлагает Machine Accounts. Это сервисные учётки для автоматизации.
В Bitwarden (Secrets Manager) создаём Machine Account.
В админке — New → Machine account
Назначаем доступ: проектам dev и prod, права read (или read, write если нужно)
Внутри аккаунта генерируем Access Token — он показывается один раз, сохраняем его.
С этого момента можно логиниться через CLI двумя способами:
Один раз задать переменную окружения:
export BWS_ACCESS_TOKEN="ваш_токен_доступа"
bws secret list
Или использовать токен при каждом запросе:
bws secret list --access-token "ваш_токен_доступа"
Осталось научиться подгружать энвы из Bitwarden и подружить это с пайплайном наших приложений.
Автоматическая подгрузка ключей через sh скрипт
Вместо того, чтобы руками собирать .env, можно подружить bws с запуском контейнера. Для этого пишем sh-скрипт и делаем его точкой входа.
1. Скрипт для бэкенда
#!/bin/sh
set -e
# 1. Настраиваем сервер Bitwarden (кластер или self-host)
bws config server-base https://vault.bitwarden.eu
# 2. Определяем проект по NODE_ENV
if [ "$NODE_ENV" = "development" ]; then
PROJECT_ID="$BW_PROJECT_DEV"
else
PROJECT_ID="$BW_PROJECT_PROD"
fi
# 3. Загружаем ключи из Bitwarden Secrets Manager
SECRETS=$(bws secret list $PROJECT_ID --output json)
# 4. Сохраняем энвы в формате KEY=VALUE
TEMP_FILE=$(mktemp)
echo "$SECRETS" | jq -r '.[] | "\(.key)=\(.value)"' > "$TEMP_FILE"
# 5. Экспортируем в окружение контейнера
while IFS='=' read -r key value; do
if [ -n "$key" ] && [ -n "$value" ]; then
export "$key"="$value"
fi
done < "$TEMP_FILE"
rm "$TEMP_FILE"
# 6. Запускаем приложение
exec node dist/main
Что тут происходит по шагам
Критически важно указать сервер Bitwarden перед тем, как работать с bws:
bws config server-base
https://vault.bitwarden.eu
При регистрации в Bitwarden Cloud вы выбираете кластер, в котором будут храниться данные:
Если вы используете self-host версию, то указываете адрес вашего инстанса.
Без этой настройки bws не будет знать, куда ходить за ключами. В документации этот момент упоминается вскользь, и легко на него наткнуться лбом (как это было у меня 🙂).
Выбор проекта по окружению — логика простая: если NODE_ENV=development, берём BW_PROJECT_DEV, иначе — BW_PROJECT_PROD. Так одним образом деплоятся и dev, и prod
Загрузка секретов —
bws secret list
возвращает JSON с ключами и значениями.Парсим JSON — через jq приводим его к виду KEY=VALUE.
Экспортируем переменные — скрипт обходит файл и выставляет всё в окружение текущего контейнера.
Стартуем приложение —
exec node dist/main
заменяет shell-процесс на Node.js (чтобы Docker считал главным процессом контейнера именно приложение, а не оболочку).
2. Изменения в Dockerfile (бэкенд и фронт)
FROM node:22-slim
WORKDIR /app
# Устанавливаем утилиты, необходимые для bws
RUN apt-get update && apt-get install -y curl unzip jq && rm -rf /var/lib/apt/lists/*
# Устанавливаем Bitwarden SDK CLI (bws)
RUN curl -L "https://github.com/bitwarden/sdk-sm/releases/download/bws-v1.0.0/bws-x86_64-unknown-linux-gnu-1.0.0.zip" -o /tmp/bws.zip \
&& unzip /tmp/bws.zip -d /usr/local/bin \
&& chmod +x /usr/local/bin/bws \
&& rm /tmp/bws.zip
# Копируем скрипт entrypoint и делаем исполняемым
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Так при старте контейнера первым делом выполнится скрипт, а после — запустится само приложение.
Так же пришлось заменить базовый образ на node:22-slim вместо node:22-alpine.
bws — это заранее скомпилированный бинарник под Linux glibc. Если попытаться запустить его на Alpine (musl), бинарник не найдёт нужные функции в системе и выдаст ошибку:
/usr/local/bin/bws: /lib/ld-musl-x86_
64.so
.1: bad ELF interpreter: No such file or directory
3. Интеграция с Coolify
Сначала получаем ID наших Bitwarden проектов:
Можно взять их из браузера открыв конкретный проект. В адресной строке будет что-то вроде:
https://vault.bitwarden.eu/#/projects/abcd1234-5678-90ef-ghij-klmnopqrstuv
Вот этот длинный UUID и есть ID проекта (abcd1234-5678-90ef-ghij-klmnopqrstuv).Либо можно достать список проектов из bws: команда
bws project list
вернёт JSON со всеми проектами, где будут поля id и name. Например:
[
{
"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
"name": "dev"
},
{
"id": "wxyz9876-5432-10lk-jihg-fedcba098765",
"name": "prod"
}
]
В Configuration → Environment Variables для приложения добавляем:
BWS_ACCESS_TOKEN=ваш_access_token
BW_PROJECT_DEV=<id_dev_проекта>
BW_PROJECT_PROD=<id_prod_проекта>
Coolify пробросит их в контейнер, а entrypoint.sh подхватит и подтянет правильные переменные.
Нюансы интеграции с Vite
Для фронтенда на Vite Dockerfile остаётся таким же, как у бэкенда: используем node:22-slim, устанавливаем bws, копируем entrypoint.sh, ставим права и указываем его как ENTRYPOINT.
А вот скрипт отличается, потому что фронтенд — это статический билд, и переменные окружения нужно подставлять в HTML на этапе старта контейнера.
Для корректной подстановки переменных я добавил библиотеку vite-plugin-runtime-env — с ней process.env работает динамически даже в собранных Vite файлах.
Скрипт entrypoint.sh для Vite
#!/bin/sh
set -e
# 1. Настраиваем Bitwarden сервер
bws config server-base https://vault.bitwarden.eu
# 2. Выбираем проект по NODE_ENV
if [ "$NODE_ENV" = "development" ]; then
PROJECT_ID="$BW_PROJECT_DEV"
else
PROJECT_ID="$BW_PROJECT_PROD"
fi
# 3. Загружаем секреты из Bitwarden
SECRETS=$(bws secret list $PROJECT_ID --output json)
# 4. Экспортируем переменные окружения
TEMP_FILE=$(mktemp)
echo "$SECRETS" | jq -r '.[] | "\(.key)=\(.value)"' > "$TEMP_FILE"
while IFS='=' read -r key value; do
if [ -n "$key" ] && [ -n "$value" ]; then
export "$key"="$value"
fi
done < "$TEMP_FILE"
rm "$TEMP_FILE"
# 5. Подставляем переменные в dist/index.html
if command -v npx >/dev/null 2>&1; then
npx --yes envsub dist/index.html || echo "[WARN] envsub failed; proceeding with original index.html"
else
echo "[WARN] npx not found; skipping env substitution"
fi
# 6. Запуск фронтенда
exec serve -s dist -l tcp://0.0.0.0:3000 -n
Для подстановки переменных в HTML здесь используется envsub (и поддержка vite-plugin-runtime-env), чтобы динамические значения попадали в index.html.
После того как скрипты настроены и Dockerfile обновлён, всё готово к запуску. Ручной ввод ключей в админке Coolify нужен всего один раз — чтобы задать три переменные: токен Machine Account и ID проектов для dev и prod.
При следующем деплое контейнер автоматически подтянет все ключи из Bitwarden, подставит их в окружение (и, для фронтенда, в HTML), а ручные ключи можно удалить.
Теперь можно деплоить как обычно: CI/CD пробрасывает токен Machine Account, entrypoint.sh подхватывает ключи, и приложение стартует с актуальными значениями.
Заключение
Мы рассмотрели, как подружить Bitwarden CLI с деплоем приложений на NestJS и Vite через Docker и Coolify, разобрали нюансы entrypoint‑скриптов, подстановку переменных и особенности фронтенда.
Буду рад, если этот гайд окажется полезным, и благодарен за обратную связь, вопросы или советы по улучшению процесса.