Containers: Linux primitives (namespaces, cgroups, overlayfs), OCI standard, runc/containerd/CRI-O, image layers + registry, rootless containers, gVisor/Kata/Firecracker for harder isolation. Four scenarios: lifecycle (build->push->pull->run->cleanup), namespace isolation demo, image layers COW, rootless vs sandbox.
«Container = Docker» — это леджендарное упрощение, из-за которого падают деплои. Контейнер — это обычный Linux-процесс, которому kernel дал свой набор namespaces и cgroups. Docker, containerd, runc — это разные слои абстракции над одними и теми же примитивами ядра. Без понимания того, что происходит «под капотом», нельзя осмысленно ответить:
memory.max=4Gi и не падает?docker run ~50ms, а VM — 60 секунд?kubectl exec работает, а docker exec на той же ноде — нет?Контейнеры — фундаментальный билдинг-блок современной инфраструктуры (Kubernetes, serverless, CI/CD, edge). Если не понимаешь примитивы — будешь писать --privileged «чтобы заработало» и однажды сольёшь кластер.
Контейнер = процесс с изолированным namespace + лимитами cgroup. Docker — удобный wrapper. OCI — стандарт, чтобы контейнеры были взаимозаменяемы между runtime'ами.
Три уровня, которые надо чётко разделять в голове:
config.json) и вызывает clone(), setns(), pivot_root(), пишет в cgroupfs. Маленькая, заменяемая.Ключевая истина: изоляция в kernel. Все красивые UX-фичи Docker — это сахар. Когда kernel падает или у тебя CVE — падают все контейнеры на этом host'е, потому что kernel один на всех. Это и принципиальное отличие от VM (где у каждого гостя свой kernel).
Диаграмма показывает полный стек от Dockerfile до running PID 1 и параллельно — границы изоляции.
Левый верх — Container runtime stack. Orchestrator (Kubernetes) → containerd через CRI gRPC → runc через OCI runtime spec → Linux kernel через clone()/setns()/cgroup write. Это та самая цепочка вызовов, которую теперь видит каждый k8s-кластер после удаления dockershim (k8s 1.24+). Раньше было лишнее звено kubelet → dockershim → docker → containerd → runc, сейчас прямо kubelet → containerd → runc.
Правый верх — Container A и Container B. Два контейнера на одном kernel, у каждого — свой PID namespace (видит только свои PIDs), свой network namespace (свой eth0, IP, routing table, conntrack), свой mount namespace (overlayfs rootfs). Cgroups: memory.max=2Gi для A, memory.max=512Mi для B. Каждое из этих ограничений — отдельная нода в группе, чтобы было видно, что изоляция — это композиция примитивов, а не один магический «container» объект.
Левый низ — Image / build / registry. Dockerfile разбирается BuildKit на ordered layers (base alpine → pip install → COPY .), каждый layer content-addressable (sha256), пушится в OCI registry (ECR/Harbor/GHCR), pull-ится на ноду через containerd. Overlayfs склеивает lower (read-only image layers, shared между контейнерами) + upper (writable per-container).
Правый низ — Sandbox runtimes. gVisor (Sentry — userspace re-implementation Linux syscalls на Go), Kata (pod в lightweight VM с отдельным guest kernel), Firecracker (AWS microVM для Lambda/Fargate). Все три — альтернативы runc для случаев, когда shared kernel — недостаточная изоляция.
Полный путь от Dockerfile до running PID 1. Показывает, кто что делает на каждом шаге. BuildKit разбирает Dockerfile на content-addressable layers, registry хранит каждый layer отдельно (deduplication), containerd на ноде pull-ит только новые. Дальше runc делает один атомарный clone(CLONE_NEWPID|NEWNET|NEWNS|NEWUTS|NEWIPC|NEWUSER), kernel создаёт namespaces, attach к cgroup, exec /app/main, и через ~100мс у тебя PID 1. Cleanup: SIGTERM → grace period → SIGKILL, kernel освобождает namespaces и cgroup, overlayfs upper layer удаляется (если не named volume).
Главный takeaway: build cache работает по sha256 layers, и порядок инструкций в Dockerfile определяет, что инвалидируется при изменении кода. COPY . . до RUN pip install = 50MB пересборка на каждый коммит.
Два контейнера на одном kernel — что видят, чего не видят. ps aux внутри Container A видит только свой PID 1 + дочерние процессы. На host этот же PID — 28471. Mapping односторонний: host видит всё, контейнер — только своё. ip a внутри Container A показывает eth0 с IP 172.17.0.5 (адрес в docker bridge network), не host IP. / — это overlayfs, не host root.
Container B вообще не знает, что Container A существует. Не может curl 172.17.0.5, если нет docker network connect (отдельный conntrack).
Но: kernel общий. Cgroup memory.max=512Mi для Container B держится kernel'ом, не runtime'ом — даже если runc умрёт, лимит остаётся. Если процесс жрёт больше — kernel OOM killer убивает PID 1 в этом cgroup, контейнер получает exit code 137 (128+9), kubelet видит OOMKilled, рестартит pod.
И здесь же — главный риск: kernel CVE (Dirty Pipe CVE-2022-0847, Dirty Cow) = container escape. Mitigation: non-root user внутри (USER 1000), drop ALL capabilities, seccomp default profile (блокирует ~50 опасных syscalls), read-only rootfs, никогда --privileged.
Почему 100 контейнеров не занимают 100× места. Image = manifest + ordered immutable layers + config. Layers content-addressable (sha256), хранятся на ноде по своему хешу. Overlayfs делает union mount: lowerdir=base:deps:app (read-only, shared между всеми контейнерами этого image), upperdir=container-A-rw (writable, persontal). Reads берутся из union view, writes уходят в upperdir (Copy-on-Write — файл копируется в upper только при первой записи).
Math: image 62MB (7+50+5) × 100 replicas ≠ 6.2GB. Lower layers shared → 62MB на диске один раз, upperdir типично 1-10MB per контейнер → итого ~200-1000MB.
Build cache работает на том же механизме: меняешь main.py → layer-base hit (cached), layer-deps hit (requirements.txt не изменился), layer-app rebuild (тонкий 5MB). Cleanup: docker rm удаляет только upperdir, lower layers остаются (могут использоваться другими контейнерами); docker image prune чистит unreferenced.
Пять уровней изоляции от ужасного до параноидального:
--privileged + bind-mount / → никакой изоляции, тривиальный escape (cat /host/etc/shadow). НИКОГДА в production./proc).ADR-001: Docker engine vs containerd / CRI-O в production. В Kubernetes 1.24+ dockershim удалён — Docker daemon больше не CRI-совместимый runtime. Команды паникуют: «надо мигрировать с Docker на containerd». На самом деле Docker сам поверх containerd работает с 2017 года (containerd был extracted из Docker). Меняется только то, что kubelet говорит с containerd напрямую через CRI socket, без лишнего Docker shim. Образы те же (OCI image spec), runc тот же, namespaces/cgroups те же. Решение: в production Kubernetes использовать containerd (или CRI-O) напрямую — убирает один hop (kubelet → dockershim → docker → containerd → runc становится kubelet → containerd → runc), экономит ~100MB/node RAM, убирает один daemon, который может упасть. Docker desktop / docker CLI оставить для dev-машин (UX лучше). Никогда не вкомпиливать docker-specific фичи (docker exec на pods) в production tooling — использовать kubectl exec / crictl.
ADR-002: Когда нужен gVisor / Kata вместо обычного runc. Контейнеры разделяют kernel — kernel CVE = container escape. Для multi-tenant (CI runners untrusted code, serverless, shared SaaS dataplane) это unacceptable risk. Варианты: (1) runc + defence-in-depth — shared kernel остаётся; (2) gVisor — userspace kernel, ~5-15% overhead, compatibility-friendly но не все syscalls; (3) Kata — pod в VM с своим kernel, +50-100MB/pod, boot ~200ms; (4) Firecracker — AWS Lambda/Fargate, boot ~125ms, full kernel isolation. Решение: single-tenant trusted (свои workloads) → runc с hardening (non-root, read-only FS, seccomp default, AppArmor, cap-drop ALL, никогда --privileged). Multi-tenant (customer code в CI, serverless, shared dataplane) → Kata или Firecracker. gVisor только когда нужен compatibility-friendly userspace sandbox. Платить performance overhead проще, чем объяснять customer, почему его данные утекли через kernel exploit.
--mount=type=cache).--privileged «чтобы заработало» — cap-add ALL + mount /:/host + access to host devices. Container escape становится trivial. Если правда нужна capability — добавляй точечно (--cap-add NET_ADMIN).USER в Dockerfile не задан → UID=0 → если процесс пробьёт изоляцию, у него root на host (для rootful runtime). Решение: USER 1000 в Dockerfile + runAsNonRoot: true в PodSecurityContext./var/log/app.log теряется на рестарте + не видны kubectl logs. 12-factor: лог в stdout/stderr, runtime собирает.resources.limits (минимум memory).myapp:latest). При rollback не знаешь, какой был image. Использовать SHA digest (myapp@sha256:...) или semver (myapp:v1.2.3).tini / dumb-init или родной init контейнера (k8s shareProcessNamespace).readOnlyRootFilesystem: true, writable только nested volumes (/tmp, /var/cache). Защита от persistent malware.docker exec в production runbook'ах после миграции на k8s. На worker node нет Docker daemon → не работает. Использовать kubectl exec или crictl exec.namespaces(7), cgroups(7), capabilities(7), seccomp(2), user_namespaces(7). Первоисточник.dockershim удалили и что с этим делать.::concept{slug="virtualization"}, ::concept{slug="12-factor-app"}, ::concept{slug="kubernetes-fundamentals"}, ::concept{slug="service-mesh"}.::case{slug="kubernetes-cluster"} — orchestration in practice; ::case{slug="ci-cd-pipeline"} — image building & registry; ::case{slug="serverless-platform"} — Firecracker / microVMs.