API-ключ в Docker: как передать безопасно и не запечь в образ
Ключ в Dockerfile остаётся в слоях образа навсегда. Разбираем env_file, секреты Compose, секреты при сборке, .dockerignore и проверку образа — с примерами для ключа Zukko.
5 мин · · разработка · API-ключи
API-ключ в Docker чаще всего утекает не через взлом, а через сам образ: ключ записали в ENV, скопировали .env внутрь или передали через ARG — и он остался в слоях, которые уезжают в реестр. Ниже — способы передать ключ контейнеру только во время запуска, от простого docker run -e до секретов Docker Compose, и как проверить, что в образе ничего не осталось. Примеры — для ключа Zukko и OpenAI SDK, но подход одинаков для любого ключа.
Чего не делать в Dockerfile
ENV OPENAI_API_KEY=fetch_ваш_ключ
ARG OPENAI_API_KEY
COPY .env /app/.envВсе три строки плохие. ENV сохраняется в конфигурации образа и видна любому, кто его скачал. По документации Docker, аргументы сборки и переменные окружения не подходят для секретов, потому что остаются в итоговом образе. COPY .env кладёт файл в слой; даже если удалить его следующей командой, предыдущий слой с файлом сохранится. И, конечно, ключ не должен быть в исходном коде.
Правило простое: образ собирается без ключа, ключ появляется только у запущенного контейнера.
Способ 1: переменная при запуске
docker run -e OPENAI_API_KEY="fetch_ваш_ключ" \
-e OPENAI_BASE_URL="https://api.zukko.pro/v1" myappPython-клиент OpenAI сам читает переменные OPENAI_API_KEY и OPENAI_BASE_URL, поэтому в коде достаточно:
from openai import OpenAI
client = OpenAI()Если хотите явности, передайте значения аргументами: OpenAI(base_url=os.environ["OPENAI_BASE_URL"], api_key=os.environ["OPENAI_API_KEY"]).
Минусы: ключ попадает в историю командной оболочки, а переменные окружения запущенного контейнера видны через docker inspect любому, у кого есть доступ к Docker на сервере.
Способ 2: файл с переменными (env_file)
Создайте на сервере файл zukko.env:
OPENAI_API_KEY=fetch_ваш_ключ
OPENAI_BASE_URL=https://api.zukko.pro/v1
Запуск без Compose:
docker run --env-file zukko.env myappВ Docker Compose то же самое делает атрибут env_file:
services:
api:
image: myapp:latest
env_file:
- zukko.envПуть указывается относительно compose.yaml. Файл не коммитится в репозиторий и не попадает в контекст сборки (см. раздел про .dockerignore). На сервере ограничьте доступ к нему: chmod 600 zukko.env.
Ключ при этом всё равно становится переменной окружения контейнера — со всеми минусами из предыдущего раздела.
Способ 3: секреты Docker Compose
Документация Docker прямо рекомендует секреты вместо переменных окружения для паролей и API-ключей: переменные доступны всем процессам и могут случайно попасть в логи. Секрет монтируется в контейнер файлом /run/secrets/<имя_секрета> и доступен только тем сервисам, которым вы его явно выдали.
services:
api:
image: myapp:latest
environment:
OPENAI_BASE_URL: https://api.zukko.pro/v1
secrets:
- zukko_api_key
secrets:
zukko_api_key:
file: ./secrets/zukko_api_key.txtФайл ./secrets/zukko_api_key.txt содержит одну строку — ключ. Вместо файла источником может быть переменная окружения на хосте, где запускается Compose: environment: ZUKKO_API_KEY в описании секрета.
В приложении ключ читается из файла:
import os
from pathlib import Path
from openai import OpenAI
def read_api_key() -> str:
secret = Path("/run/secrets/zukko_api_key")
if secret.exists():
return secret.read_text().strip()
return os.environ["OPENAI_API_KEY"]
client = OpenAI(base_url="https://api.zukko.pro/v1", api_key=read_api_key())Запасной вариант через переменную удобен для локальной разработки без Compose.
Если ключ нужен во время сборки
Обычно ключ LLM при сборке не нужен. Но если сборка, например, генерирует данные через API, используйте секреты сборки — они доступны только на время одной инструкции RUN и не остаются в образе:
RUN --mount=type=secret,id=zukko \
OPENAI_API_KEY="$(cat /run/secrets/zukko)" python scripts/prepare.pydocker build --secret id=zukko,src=./secrets/zukko_api_key.txt -t myapp ..dockerignore и .gitignore
При docker build весь контекст (обычно текущая папка) отправляется сборщику. Если там лежит .env, его может подхватить неосторожный COPY . .. Файл .dockerignore исключает такие файлы из контекста:
.env
.env.*
*.env
secrets/
.git
В .gitignore добавьте то же самое (.env, *.env, secrets/). Сам .dockerignore при этом коммитится — он должен быть у всех, кто собирает образ.
Отдельные ключи для окружений
В Zukko можно создать несколько ключей и задать каждому дневной и месячный лимит в долларах и список разрешённых моделей — подробнее в разделе лимиты ключа. Для Docker это удобно:
- ключ для разработки — небольшой дневной лимит и только дешёвая модель, например
glm-5.3-flash; - ключ для staging — свой лимит;
- ключ для продакшена — дневной и месячный лимиты под реальную нагрузку.
Каждому окружению — свой файл секрета. Если ключ утёк, удалите его на странице API-ключи, создайте новый, обновите файл секрета и перезапустите сервис: docker compose up -d. Пересобирать образ не нужно — в нём ключа нет. Полный ключ показывается один раз при создании, поэтому сразу сохраните его в файл секрета или менеджер паролей.
Если ключ превысит лимит, запросы будут отклоняться с кодом 429, а модель вне списка разрешённых вернёт 403 model_not_allowed_for_key — это видно в логах контейнера.
Kubernetes
В Kubernetes ключ хранят в объекте Secret и подключают к поду как переменную или как файл через volume:
kubectl create secret generic zukko-api-key \
--from-file=api_key=./secrets/zukko_api_key.txt -n productionenv:
- name: OPENAI_BASE_URL
value: "https://api.zukko.pro/v1"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: zukko-api-key
key: api_keyУчтите, что по умолчанию значения Secret хранятся в base64, а не в зашифрованном виде; настройки шифрования и доступа смотрите в документации Kubernetes.
Как проверить, что ключ не попал в образ
docker history --no-trunc myapp | grep -i "fetch_"
docker image inspect myapp --format '{{json .Config.Env}}'Первая команда ищет ключ в командах слоёв, вторая показывает переменные, зашитые в образ. Ключа не должно быть ни там, ни там. Если нашли — удалите ключ в кабинете, исправьте Dockerfile и пересоберите образ; уже запушенные теги удалите из реестра.
Частые вопросы
Что безопаснее: env_file или секреты Compose?
Секреты. Значение из env_file становится переменной окружения и видно через docker inspect и всем процессам контейнера. Секрет — это файл, доступный только сервисам, которым он выдан.
Можно ли передать ключ через ARG при сборке?
Нет: аргументы сборки остаются в итоговом образе. Если ключ нужен при сборке, используйте docker build --secret и RUN --mount=type=secret.
Какой base URL указывать в контейнере?
Для OpenAI SDK и OpenAI-совместимых клиентов — https://api.zukko.pro/v1. Для официального Anthropic SDK — https://api.zukko.pro без /v1, SDK добавит его сам. Подробнее — в быстром старте.
Как часто менять ключ?
Сразу при подозрении на утечку и при уходе человека, у которого был доступ. Благодаря секретам смена ключа — это замена одного файла и перезапуск сервиса.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Gemini API на Python: запросы через OpenAI SDK и OpenAI-форматРабочий пример Gemini API на Python без отдельной библиотеки: меняем base_url в OpenAI SDK, добавляем стриминг и JSON и сравниваем с google-genai.
- Anthropic SDK для Python: base_url, ключ и первый запрос через шлюзОфициальный пакет anthropic для Python нужно только направить на другой адрес. Разбираем base_url, ключ, стриминг, подсчёт токенов, кэш и ошибки.