Docker и контейнеры
Как упаковать .NET-приложение в контейнер и запустить его рядом с зависимостями и в кластере: чем контейнер отличается от виртуальной машины, как собрать маленький и безопасный образ, как поднять стенд через docker compose и что Kubernetes ждёт от приложения. На собеседовании это проверяют вопросами про слои, порты, пробы и лимиты.
Перед этой темой: Хостинг и конвейер.
Что такое контейнер и чем он отличается от виртуальной машины?
Заголовок раздела «Что такое контейнер и чем он отличается от виртуальной машины?»Что это
- Образ — неизменяемый шаблон: файлы приложения, рантайм, системные библиотеки и команда запуска.
- Контейнер — запущенный экземпляр образа: процесс со своей файловой системой, сетью и лимитами.
- Dockerfile — текстовый рецепт сборки образа из инструкций
FROM,COPY,RUN,ENTRYPOINT. - Реестр — хранилище образов: Docker Hub,
mcr.microsoft.com, Azure Container Registry.
Зачем
Один артефакт работает одинаково на ноутбуке, в CI и на сервере: рантайм нужной версии уже внутри образа.
Как пользоваться
docker build— собрать образ по Dockerfile.docker run -p 5000:8080— запустить: порт 5000 хоста ведёт в порт 8080 контейнера.-e— передать переменную окружения,-v— подключить том, чтобы данные пережили пересоздание контейнера.
Частая ошибка
Хранить данные внутри контейнера: загруженные файлы, SQLite, логи. После docker rm или обновления образа они исчезают.
Как устроено
- Слои. Каждая инструкция
RUN,COPYилиADDдаёт слой — набор изменений файловой системы. Слои только для чтения, адресуются хешем содержимого и общие для разных образов. - Запуск. Docker накладывает слои друг на друга (overlayfs) и добавляет сверху записываемый слой контейнера.
- Изоляция. Namespaces дают процессу свои PID, сеть, точки монтирования и hostname: приложение внутри — PID 1. Cgroups ограничивают CPU, память и число процессов.
- Порты. У контейнера свой сетевой стек.
EXPOSEв Dockerfile — только документация, доступ снаружи открывает-p хост:контейнер.
Когда что выбирать
| Признак | Контейнер | Виртуальная машина |
|---|---|---|
| Ядро ОС | общее с хостом | своё |
| Старт | доли секунды | десятки секунд |
| Размер | десятки–сотни МБ | гигабайты |
| Изоляция | процессная | аппаратная, сильнее |
Ловушки
- Том или bind mount. Именованным томом (
-v data:/data) управляет Docker. Bind mount (-v ./conf:/app/conf) подключает папку хоста — удобно в разработке, но привязывает к конкретной машине. - Linux-контейнеры на Windows. Docker Desktop запускает их в лёгкой ВМ WSL 2: контейнеру всё равно нужно ядро Linux.
- Тег — не версия.
latestи даже1.0можно перезаписать. Неизменяемая ссылка — digest@sha256:....
Правило
Контейнер одноразовый: настройки — в переменных окружения, данные — в томах или внешних сервисах, логи — в stdout.
Под капотом
- Цепочка запуска: Docker CLI → dockerd → containerd → runc. Формат образа и рантайма описан стандартом OCI, поэтому тот же образ запускают Podman и Kubernetes.
- Namespaces: pid, net, mnt, uts, ipc, user, cgroup. Cgroups v2 задают
cpu.maxиmemory.max: за превышение памяти ядро убивает процесс, за превышение CPU — троттлит. - PID 1 получает сигналы.
ENTRYPOINT ["dotnet", "Shop.Api.dll"](exec-форма) принимает SIGTERM напрямую. Shell-форма запускает приложение под/bin/sh -c, сигнал до него не доходит, иdocker stopчерез 10 секунд убивает процесс. - Windows-контейнеры (Server Core, Nano Server) есть, в том числе с IIS и .NET Framework, но работают только на Windows-хосте и весят гигабайты.
Цена
- Общее ядро — общая поверхность атаки. Контейнер с
--privilegedили смонтированнымdocker.sockфактически равен root на хосте. - Запись в слой контейнера идёт через copy-on-write и медленнее записи в том.
Компромиссы
- Плотность и скорость доставки против силы изоляции: для своего кода контейнеров хватает, для чужого нужны ВМ или gVisor и Kata Containers.
- Неизменяемый образ упрощает откат — запустить предыдущий тег, — но конфигурацию и состояние приходится выносить наружу.
Как проверить в проде
docker inspect— лимиты, монтирования, переменные;docker stats— CPU и память относительно лимитов.docker history— какие инструкции дали самые тяжёлые слои.- Код выхода 137 (128 + 9) — процесс убит SIGKILL: по OOM или после таймаута остановки.
Итог: контейнер — процесс хоста с ограниченной видимостью и ресурсами, и его сильные и слабые стороны следуют из общего ядра.
# Собрать образ по Dockerfile из текущей папкиdocker build -t shop-api:1.0 .
# Порт хоста 5000 → порт 8080 в контейнере, переменная, том и лимитыdocker run -d --name shop-api -p 5000:8080 \ -e ASPNETCORE_ENVIRONMENT=Staging \ -v shop-uploads:/app/uploads \ --memory 512m --cpus 1.5 \ shop-api:1.0
docker ps # запущенные контейнерыdocker logs -f shop-api # stdout приложенияdocker stop shop-api # SIGTERM, через 10 секунд — SIGKILLdocker rm shop-api # контейнер удалён, том shop-uploads остался- Чем образ отличается от контейнера? — Образ — неизменяемый шаблон, контейнер — его запущенный экземпляр со своим записываемым слоем; из одного образа — сколько угодно контейнеров.
- Что будет с файлами, записанными внутрь контейнера? — Живут до удаления контейнера; переживают его только тома и bind mount.
- Зачем
EXPOSE, если есть-p? —EXPOSEдокументирует порт, а публикует его-p(или-Pдля всех портов изEXPOSE). - Почему приложение не останавливается корректно по
docker stop? — Shell-формаENTRYPOINT: SIGTERM получаетsh, а не приложение. - Можно ли запустить Linux-контейнер на Windows? — Да, через Docker Desktop с WSL 2: ядро Linux работает в лёгкой ВМ.
Как правильно собрать образ .NET-приложения?
Заголовок раздела «Как правильно собрать образ .NET-приложения?»Что это
Многоэтапный Dockerfile — несколько FROM в одном файле. Каждый этап — отдельный образ, а в итог попадает только последний этап и то, что в него скопировали через COPY --from.
Зачем
SDK нужен только для сборки, в проде это сотни лишних мегабайт и компилятор под рукой у атакующего. Простой Web API на aspnet:10.0 весит около 90 МБ в сжатом виде.
Как пользоваться
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build— этап сборки.- Скопировать
.csprojи выполнитьdotnet restore— пакеты попадают в отдельный слой. - Скопировать код и выполнить
dotnet publish -o /app. FROM mcr.microsoft.com/dotnet/aspnet:10.0, затемCOPY --from=build /app .иENTRYPOINT.- В
.dockerignoreперечислитьbin/иobj/, чтобы они не попадали в контекст сборки.
Частая ошибка
Пробрасывать порт 80 по привычке. С .NET 8 приложение в официальном образе слушает 8080 (ASPNETCORE_HTTP_PORTS=8080), и -p 5000:80 не работает.
Как устроено
- Кеш слоёв. Docker переиспользует слой, если не изменились инструкция и входные файлы, а после первого изменённого пересобирает все следующие. Поэтому
.csprojкопируют до кода: правка в.csне запускает restore. - Образы Microsoft. Теги
10.0указывают на Ubuntu 24.04 (Noble); образов на Debian для .NET 10 нет.
| Образ | Что внутри | Для чего |
|---|---|---|
sdk:10.0 |
SDK, MSBuild, компиляторы | сборка и тесты |
aspnet:10.0 |
рантайм .NET и ASP.NET Core | веб-приложения |
runtime:10.0 |
рантайм .NET | консоль, worker |
runtime-deps:10.0 |
только системные библиотеки | self-contained, Native AOT |
Когда что выбирать
- Chiseled (
aspnet:10.0-noble-chiseled) — distroless-образ Ubuntu без shell и пакетного менеджера, non-root по умолчанию. ICU и tzdata нет, включён режим инвариантной глобализации; если нужны культуры и часовые пояса —-noble-chiseled-extra. - Composite (
-noble-chiseled-composite) — фреймворк собран в один ReadyToRun-файл: образ меньше, но версия фреймворка жёстко привязана. - Без Dockerfile —
dotnet publish /t:PublishContainer: SDK сам берёт базовый образ по типу проекта, пользователяappи порт; вариант задаётContainerFamily.
Ловушки
- В обычных образах пользователь
appесть, но процесс работает от root, пока не указатьUSER $APP_UID. А non-root не может писать в/appи в тома, созданные root. HEALTHCHECKчерезcurlв chiseled-образе не работает: нет ни curl, ни shell.
Правило
Слои — от редко меняющихся к часто меняющимся; финальный образ — минимальный, non-root, с явной версией.
Под капотом
- BuildKit, сборщик Docker по умолчанию, выполняет независимые этапы параллельно.
RUN --mount=type=cache,target=/root/.nuget/packagesсохраняет кеш NuGet между сборками, даже когда слой restore пересобирается. FROM --platform=$BUILDPLATFORMиdotnet publish -a $TARGETARCHсобирают образ для arm64 на x64-агенте без эмуляции: SDK работает на родной архитектуре.PublishContainerне нужен Docker-демон: SDK скачивает слои базового образа, добавляет слой с приложением и отправляет образ в реестр (ContainerRegistry) или в архив (ContainerArchiveOutputPath).
Цена
Сжатый образ простого Web API, по замеру dotnet-docker на ноябрь 2025:
| Вариант | Размер |
|---|---|
aspnet:10.0 |
~92 МБ |
aspnet:10.0-noble-chiseled |
~53 МБ |
Native AOT на runtime-deps:10.0-noble-chiseled |
~12 МБ |
Native AOT даёт быстрый старт и меньше памяти, но ограничивает рефлексию и динамическую загрузку кода, а собирается в sdk:10.0-noble-aot. Подробно — в теме Платформа .NET.
Компромиссы
- Плавающий тег
10.0приносит патчи безопасности при пересборке, digest — воспроизводимость. Обычно фиксируют digest, а обновляют его Dependabot или Renovate. - Chiseled без shell не даёт зайти внутрь: отлаживают отдельным контейнером рядом (
kubectl debug) или через dotnet-monitor.
Как проверить в проде
docker history— вес каждого слоя.- Сканер уязвимостей (Trivy, Docker Scout) по образу. В .NET 10 chiseled-образы содержат Chisel manifest, по которому сканеры видят пакеты.
docker image inspect -f "{{ .Config.User }}" образ— от какого пользователя он запустится.
Итог: хороший образ — минимальный, non-root, воспроизводимый и быстро пересобираемый, а chiseled, composite и AOT — это выбор размера против совместимости.
# Этап сборки: SDK нужен только здесьFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS buildARG TARGETARCHWORKDIR /src
# Сначала только файл проекта: слой restore берётся из кеша, пока не менялись пакетыCOPY Shop.Api/Shop.Api.csproj Shop.Api/RUN dotnet restore Shop.Api/Shop.Api.csproj -a $TARGETARCH
# Потом код и публикация (Release — по умолчанию с .NET 8)COPY . .RUN dotnet publish Shop.Api/Shop.Api.csproj -a $TARGETARCH --no-restore -o /app
# Этап запуска: без shell и пакетного менеджера, non-root по умолчаниюFROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseledWORKDIR /appCOPY --from=build /app .EXPOSE 8080ENTRYPOINT ["dotnet", "Shop.Api.dll"]# SDK сам возьмёт aspnet:10.0-noble-chiseled, пользователя app и порт 8080dotnet publish Shop.Api --os linux --arch x64 /t:PublishContainer \ -p ContainerRepository=shop-api \ -p ContainerFamily=noble-chiseled \ -p ContainerImageTag=1.4.2
# Образ уже в локальном Dockerdocker run -p 5000:8080 shop-api:1.4.2- Зачем копировать
.csprojотдельно от кода? — Чтобы слой restore брался из кеша, пока не менялись зависимости. - Почему приложение на .NET 8+ в контейнере не отвечает на порту 80? — Порт по умолчанию — 8080 (
ASPNETCORE_HTTP_PORTS): непривилегированный порт нужен для работы без root. - Чем chiseled отличается от Alpine? — Оба без ICU по умолчанию; Alpine — musl, shell и apk, chiseled — glibc Ubuntu без shell и пакетного менеджера.
- Когда
PublishContainerне подойдёт? — Когда в образе нужныRUN-шаги: системные пакеты, сертификаты. Тогда Dockerfile или свой базовый образ. - Как отлаживать контейнер без shell? — Отладочный контейнер рядом (
kubectl debug), dotnet-monitor как sidecar, дампы в том.
Как поднять приложение с зависимостями через docker compose?
Заголовок раздела «Как поднять приложение с зависимостями через docker compose?»Что это
Compose-файл compose.yaml перечисляет сервисы — контейнеры с образом, портами, переменными и томами. docker compose up -d создаёт и запускает их все, docker compose down останавливает и удаляет контейнеры, а тома оставляет.
Зачем
Разработчик одной командой получает API, PostgreSQL и Redis нужных версий, ничего не устанавливая на машину.
Как пользоваться
- Опишите сервисы
api,db,cache. - В строке подключения API укажите имя сервиса:
Host=db;Port=5432. - Данные базы положите в именованный том.
- Наружу (
ports) публикуйте только то, к чему ходите с хоста.
Частая ошибка
Писать localhost в строке подключения из контейнера к базе. Внутри контейнера localhost — он сам, а база доступна как db.
Как устроено
- Compose читает
compose.yaml, автоматически добавляетcompose.override.yamlи подставляет${VAR}из.envи окружения. - Создаёт сеть проекта: имя сервиса становится DNS-именем. Порты между сервисами публиковать не нужно.
- Создаёт тома и запускает контейнеры в порядке
depends_on, ожидая указанных условий.
Когда что выбирать
| Задача | Средство |
|---|---|
| Дождаться готовности БД | healthcheck и condition: service_healthy |
| Обычные настройки | environment, env_file |
| Пароли и ключи | secrets — файл в /run/secrets/имя |
| Необязательные сервисы | profiles и --profile tools |
| Различия окружений | override-файлы, -f compose.yaml -f compose.prod.yaml |
Ловушки
- Короткий
depends_onждёт старта контейнера, а не готовности Postgres. healthcheckвыполняется внутри контейнера. В chiseled-образе API нетcurlи shell — такую проверку не написать.- Переменная из
environmentвидна вdocker inspect, для секретов она не годится. docker compose down -vудаляет тома вместе с данными базы.
Правило
Базовый файл — общий для всех окружений; порты, bind mount исходников и отладочные сервисы — в override или профилях.
Под капотом
- Compose v2 — плагин
docker compose, клиент Docker Engine API. Имя проекта (по умолчанию — имя папки) становится префиксом сетей, томов и контейнеров. Полеversionустарело: Compose всегда проверяет файл по последней схеме. secretsв Compose — монтирование файла в/run/secretsбез шифрования. .NET читает такие файлы черезAddKeyPerFile— см. Конфигурация и логирование.docker compose watchпо секцииdevelop.watchсинхронизирует файлы или пересобирает образ при изменении кода.
Цена
Один хост: нет распределения по узлам, автомасштабирования, переноса упавшего сервиса на другой сервер и выкладки без простоя. restart: unless-stopped перезапустит процесс, но не спасёт от отказа машины.
Компромиссы
- Compose или Aspire. Aspire (с версии 13 — без «.NET» в названии) описывает стенд кодом в AppHost:
AddPostgres,WithReference,WaitFor. Проекты .NET он запускает как обычные процессы, контейнеры — для зависимостей, даёт дашборд OpenTelemetry, аAddDockerComposeEnvironmentгенерирует compose-файл для выкладки. Compose проще и не зависит от стека; Aspire удобнее, когда в решении много сервисов на .NET. - Compose в проде разумен для одиночного сервера и внутренних инструментов. Для нескольких узлов и SLA — Kubernetes или управляемая платформа.
Как проверить в проде
docker compose config— итоговая конфигурация после слияния файлов и подстановки переменных.docker compose psпоказывает статус healthcheck,docker compose logs -f api— логи сервиса.
Итог: compose — лучший инструмент для стенда на одной машине; выше — Kubernetes, а для стенда .NET-разработчика есть ещё и Aspire.
services: api: build: . ports: - "5000:8080" environment: ASPNETCORE_ENVIRONMENT: Development # Хост базы — имя сервиса в сети compose ConnectionStrings__Shop: "Host=db;Database=shop;Username=shop" secrets: - db_password # файл /run/secrets/db_password depends_on: db: condition: service_healthy # ждать готовности, а не только старта
db: image: postgres:18 environment: POSTGRES_USER: shop POSTGRES_DB: shop POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password volumes: - pgdata:/var/lib/postgresql # с postgres:18 — без /data в конце healthcheck: test: ["CMD-SHELL", "pg_isready -U shop -d shop"] interval: 5s retries: 10
adminer: image: adminer profiles: ["tools"] # только с --profile tools ports: - "8081:8080"
volumes: pgdata:
secrets: db_password: file: ./secrets/db_password.txt- Почему
depends_onне гарантирует, что база готова? — Короткая форма ждёт только старта контейнера; нужнаcondition: service_healthy, а в приложении — повторы подключения. - Почему из контейнера API не подключиться к
localhost:5432? —localhost— сам контейнер; база доступна по имени сервиса. - Как разделить настройки разработки и продакшена? — Override-файлы, профили,
.envдля подстановки; итог проверяютdocker compose config. - Чем
secretsлучшеenvironmentдля пароля? — Файл в/run/secretsне виден вdocker inspectи не наследуется дочерними процессами. - Как поднимать базу для интеграционных тестов? — Через Testcontainers из кода теста — см. тему Тестирование.
Что нужно знать .NET-разработчику о запуске в Kubernetes?
Заголовок раздела «Что нужно знать .NET-разработчику о запуске в Kubernetes?»Что это
- Pod — минимальная единица запуска: один или несколько контейнеров с общим IP, обычно один экземпляр приложения.
- Deployment — описание «нужно 3 копии образа
shop-api:1.4.2»: создаёт поды и заменяет их при обновлении. - Service — постоянное имя и IP для набора подов, балансирует запросы между ними.
- Ingress — правила входа HTTP извне: хост, путь, TLS. Его преемник — Gateway API.
Зачем
Kubernetes сам держит нужное число копий: упал процесс или сервер — поды создаются заново.
Как пользоваться
- Добавьте в приложение эндпоинты
/healthz/liveи/healthz/ready. - Укажите их в манифесте как
livenessProbeиreadinessProbe. - Задайте
resources: сколько CPU и памяти нужно и сколько можно. - Настройки передавайте из ConfigMap и Secret.
Частая ошибка
Хранить важное в поде: файлы на диске, сессии в памяти. При следующем деплое под пересоздаётся, и данные пропадают.
Как устроено
| Проба | При сбое | Что проверять |
|---|---|---|
| startup | перезапуск; до успеха другие пробы не работают | старт завершён: миграции, прогрев |
| liveness | перезапуск контейнера | процесс жив, без внешних зависимостей |
| readiness | под убирают из Service | готов к трафику: БД и кеш доступны |
- В ASP.NET Core
AddHealthChecks()регистрирует проверки, аMapHealthChecksсPredicateпо тегам публикует разные эндпоинты. Подробно — в теме Наблюдаемость и диагностика. - По умолчанию проба выполняется каждые 10 секунд с таймаутом 1 секунда и срабатывает после 3 неудач подряд.
- Requests — резерв: по ним планировщик выбирает узел. Limits — потолок: за превышение памяти — OOMKilled, за превышение CPU — троттлинг.
Ловушки
- Liveness с проверкой БД: база легла — перезапускаются все поды, и восстановление затягивается.
- Долгий старт без startup-пробы: liveness убивает под раньше, чем он поднялся, и он уходит в
CrashLoopBackOff. - ConfigMap и Secret в переменных окружения не обновляются без перезапуска пода. Смонтированные файлы обновляются, но не при
subPath. - Secret по умолчанию лежит в etcd незашифрованным — base64 лишь кодирует.
Правило
Liveness — дешёвая и без зависимостей, readiness — про готовность к трафику, startup — для медленного старта.
Под капотом
- .NET читает cgroups v1 и v2.
Environment.ProcessorCount— минимум из числа ядер, affinity и CPU limit, округлённого вверх; requests не учитываются. Значение переопределяетDOTNET_PROCESSOR_COUNT. - Лимит памяти GC считает физической памятью,
GCHeapHardLimitпо умолчанию — 75 % лимита. Подробно — Сборка мусора и ресурсы. - При удалении пода контроллер убирает его из EndpointSlice, а kubelet параллельно выполняет preStop и шлёт SIGTERM. Пока балансировщики не обновились, запросы ещё приходят.
preStop: sleep(стабилен с Kubernetes 1.34) не требует shell — это важно для chiseled-образов. Остановку хоста разбирает Хостинг и конвейер.
Цена
- Без CPU limit под с request 0,5 CPU на 64-ядерном узле видит 64 ядра: Server GC и пул потоков настраиваются под чужие ресурсы.
- CPU limit режет квоту по периодам: всплески GC и JIT на старте упираются в троттлинг, растут хвостовые задержки.
Компромиссы
- Частый выбор — CPU request без limit плюс
DOTNET_PROCESSOR_COUNT, либо limit целым числом ядер. Для памяти limit обязателен, обычно равный request. - Stateless: сессии и кеш — в Redis, файлы — в объектном хранилище, ключи Data Protection — в общем хранилище. Иначе cookie, выданную одним подом, не расшифрует другой. Sticky sessions лишь маскируют проблему.
Как проверить в проде
kubectl describe pod— причина рестарта (OOMKilled) и сбои проб;kubectl top pod— потребление.- Метрика
container_cpu_cfs_throttled_periods_total— троттлинг; dotnet-counters —dotnet.process.cpu.countи размер кучи против лимита.
Итог: в Kubernetes .NET-сервис честно сообщает о готовности, укладывается в лимиты, которые видит рантайм, корректно останавливается и не хранит состояние в поде.
using Microsoft.AspNetCore.Diagnostics.HealthChecks;using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks() // liveness: процесс жив, внешние зависимости не проверяем .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]) // readiness: можно ли принимать трафик .AddCheck<DatabaseCheck>("database", tags: ["ready"]);
var app = builder.Build();
app.MapHealthChecks("/healthz/live", new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });app.MapHealthChecks("/healthz/ready", new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });
app.Run();
public class DatabaseCheck : IHealthCheck{ public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { // Здесь — дешёвый запрос к БД с коротким таймаутом return Task.FromResult(HealthCheckResult.Healthy()); }}apiVersion: apps/v1kind: Deploymentmetadata: name: shop-apispec: replicas: 3 selector: matchLabels: { app: shop-api } template: metadata: labels: { app: shop-api } spec: terminationGracePeriodSeconds: 45 # больше, чем preStop + ShutdownTimeout containers: - name: api image: registry.example.com/shop-api:1.4.2 ports: - containerPort: 8080 envFrom: - configMapRef: { name: shop-api-config } - secretRef: { name: shop-api-secrets } resources: requests: { cpu: "500m", memory: "512Mi" } limits: { cpu: "2", memory: "512Mi" } # ProcessorCount = 2, куча GC ≈ 384 МБ startupProbe: httpGet: { path: /healthz/live, port: 8080 } failureThreshold: 30 # до 30 × 10 секунд на старт livenessProbe: httpGet: { path: /healthz/live, port: 8080 } readinessProbe: httpGet: { path: /healthz/ready, port: 8080 } lifecycle: preStop: sleep: { seconds: 10 } # дать балансировщикам убрать под---apiVersion: v1kind: Servicemetadata: name: shop-apispec: selector: { app: shop-api } ports: - port: 80 targetPort: 8080- Чем readiness отличается от liveness? — Readiness убирает под из балансировки, liveness перезапускает контейнер.
- Почему при деплое бывают ошибки 502, хотя приложение обрабатывает SIGTERM? — Удаление из EndpointSlice идёт параллельно с SIGTERM; помогает пауза в preStop.
- Сколько ядер увидит .NET в поде с
limits.cpu: 1500m? — Два: лимит округляется вверх. - Что будет при превышении memory limit? — Ядро убьёт процесс (OOMKilled, код 137); GC старается держать кучу в пределах 75 % лимита.
- Как хранить ключи Data Protection в кластере? — В общем хранилище (Redis, БД, blob) через
PersistKeysTo..., иначе другие поды не прочитают cookie.