API-ключ в Docker: как передать безопасно и не запечь в образ

Ключ в Dockerfile остаётся в слоях образа навсегда. Разбираем env_file, секреты Compose, секреты при сборке, .dockerignore и проверку образа — с примерами для ключа Zukko.

5 мин · · разработка · API-ключи

API-ключ в Docker чаще всего утекает не через взлом, а через сам образ: ключ записали в ENV, скопировали .env внутрь или передали через ARG — и он остался в слоях, которые уезжают в реестр. Ниже — способы передать ключ контейнеру только во время запуска, от простого docker run -e до секретов Docker Compose, и как проверить, что в образе ничего не осталось. Примеры — для ключа Zukko и OpenAI SDK, но подход одинаков для любого ключа.

Чего не делать в Dockerfile

bash
ENV OPENAI_API_KEY=fetch_ваш_ключ
ARG OPENAI_API_KEY
COPY .env /app/.env

Все три строки плохие. ENV сохраняется в конфигурации образа и видна любому, кто его скачал. По документации Docker, аргументы сборки и переменные окружения не подходят для секретов, потому что остаются в итоговом образе. COPY .env кладёт файл в слой; даже если удалить его следующей командой, предыдущий слой с файлом сохранится. И, конечно, ключ не должен быть в исходном коде.

Правило простое: образ собирается без ключа, ключ появляется только у запущенного контейнера.

Способ 1: переменная при запуске

bash
docker run -e OPENAI_API_KEY="fetch_ваш_ключ" \
  -e OPENAI_BASE_URL="https://api.zukko.pro/v1" myapp

Python-клиент OpenAI сам читает переменные OPENAI_API_KEY и OPENAI_BASE_URL, поэтому в коде достаточно:

bash
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:

bash
docker run --env-file zukko.env myapp

В Docker Compose то же самое делает атрибут env_file:

bash
services:
  api:
    image: myapp:latest
    env_file:
      - zukko.env

Путь указывается относительно compose.yaml. Файл не коммитится в репозиторий и не попадает в контекст сборки (см. раздел про .dockerignore). На сервере ограничьте доступ к нему: chmod 600 zukko.env.

Ключ при этом всё равно становится переменной окружения контейнера — со всеми минусами из предыдущего раздела.

Способ 3: секреты Docker Compose

Документация Docker прямо рекомендует секреты вместо переменных окружения для паролей и API-ключей: переменные доступны всем процессам и могут случайно попасть в логи. Секрет монтируется в контейнер файлом /run/secrets/<имя_секрета> и доступен только тем сервисам, которым вы его явно выдали.

bash
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 в описании секрета.

В приложении ключ читается из файла:

bash
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 и не остаются в образе:

bash
RUN --mount=type=secret,id=zukko \
    OPENAI_API_KEY="$(cat /run/secrets/zukko)" python scripts/prepare.py
bash
docker 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:

bash
kubectl create secret generic zukko-api-key \
  --from-file=api_key=./secrets/zukko_api_key.txt -n production
bash
env:
  - 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.

Как проверить, что ключ не попал в образ

bash
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.