Initial commit: interview prep repo (resume, legend, stack cases, Kubernetes course)
This commit is contained in:
118
stack/docker/CASE.md
Normal file
118
stack/docker/CASE.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Кейс: Docker
|
||||
|
||||
Связь с легендой: [../../legend/LEGEND.md](../../legend/LEGEND.md) — раздел «АО ТНИИС», контейнеризация сервисов мониторинга и вспомогательных инструментов. Практика опирается на уже готовые стенды [../monitoring/CASE.md](../monitoring/CASE.md) и [../nginx/CASE.md](../nginx/CASE.md) — оба используют Docker Compose.
|
||||
|
||||
## Что нужно реально сделать
|
||||
|
||||
### 1. Dockerfile для собственного простого сервиса (multi-stage build)
|
||||
|
||||
```dockerfile
|
||||
# build stage
|
||||
FROM golang:1.22-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN go build -o /app ./...
|
||||
|
||||
# итоговый образ — только бинарник, без всей тулчейна сборки
|
||||
FROM alpine:3.19
|
||||
COPY --from=build /app /app
|
||||
ENTRYPOINT ["/app"]
|
||||
```
|
||||
|
||||
```
|
||||
# .dockerignore
|
||||
.git
|
||||
*.md
|
||||
node_modules
|
||||
```
|
||||
|
||||
Multi-stage build: первый этап содержит весь тяжёлый тулчейн сборки (компилятор, зависимости), второй — только финальный артефакт. Итоговый образ в разы меньше и не тащит в прод лишние инструменты сборки. Слои кешируются построчно — `COPY . .` перед `RUN build` означает, что при изменении любого файла кеш слоя сборки инвалидируется; на реальных проектах порядок команд специально выстраивают так, чтобы редко меняющиеся зависимости копировались раньше исходного кода.
|
||||
|
||||
### 2. Volumes: bind mount vs named volume
|
||||
|
||||
```bash
|
||||
# bind mount — конкретный путь на хосте, удобно для конфигов и разработки
|
||||
docker run -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro nginx
|
||||
|
||||
# named volume — управляется самим Docker, удобно для данных (БД, персистентное состояние)
|
||||
docker volume create pgdata
|
||||
docker run -v pgdata:/var/lib/postgresql/data postgres
|
||||
```
|
||||
|
||||
Bind mount используется в кейсах [../monitoring/CASE.md](../monitoring/CASE.md) и [../nginx/CASE.md](../nginx/CASE.md) для конфигов (`prometheus.yml`, `nginx.conf`) — удобно редактировать файл на хосте и сразу видеть изменения в контейнере. Named volume лучше подходит для данных, которые не нужно редактировать руками с хоста и которые должны переживать пересоздание контейнера (в [../databases/CASE.md](../databases/CASE.md) для этого пригодился бы именно named volume под `/var/lib/postgresql/data`).
|
||||
|
||||
### 3. Сети Docker: как контейнеры находят друг друга
|
||||
|
||||
```bash
|
||||
docker network ls
|
||||
docker network inspect <compose-project>_default
|
||||
```
|
||||
|
||||
В Docker Compose по умолчанию создаётся отдельная bridge-сеть на проект, и все сервисы внутри неё резолвят друг друга по имени сервиса через встроенный DNS Docker — именно поэтому в [../monitoring/CASE.md](../monitoring/CASE.md) Prometheus обращается к `node-exporter:9100`, а не по IP: имя сервиса из `docker-compose.yml` работает как hostname внутри этой сети.
|
||||
|
||||
### 4. Ограничение ресурсов и что происходит при превышении
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: myapp
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "0.5"
|
||||
memory: 256M
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run --memory=256m --cpus=0.5 myapp
|
||||
```
|
||||
|
||||
При превышении лимита памяти ядро (через cgroups, см. [../linux-bash/QUESTIONS.md](../linux-bash/QUESTIONS.md)) убивает процесс через OOM killer — контейнер завершается с кодом 137 (128 + сигнал 9 SIGKILL). Воспроизвести: запустить в контейнере с `--memory=50m` процесс, который выделяет память сверх лимита (`stress --vm 1 --vm-bytes 100M`), и увидеть код выхода 137 через `docker inspect <container> --format '{{.State.ExitCode}}'`.
|
||||
|
||||
### 5. Диагностика упавшего контейнера
|
||||
|
||||
```bash
|
||||
docker logs <container>
|
||||
docker logs --tail 50 -f <container>
|
||||
docker exec -it <container> sh
|
||||
docker inspect <container>
|
||||
docker inspect <container> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
|
||||
```
|
||||
|
||||
### 6. Внутренности: виртуализация vs контейнеризация, containerd, PID контейнера
|
||||
|
||||
Виртуализация (VM) — эмулирует полное отдельное «железо» с собственным ядром ОС через гипервизор (VMware/KVM/Hyper-V) — тяжелее, но полная изоляция вплоть до ядра. Контейнеризация — все контейнеры на хосте используют одно и то же ядро хоста, изоляция обеспечивается namespaces (что процесс видит) и ограничение ресурсов — cgroups (сколько он может использовать), без отдельного гостевого ядра — отсюда контейнеры легче и стартуют быстрее VM.
|
||||
|
||||
`containerd` — низкоуровневый контейнерный рантайм, которым Docker Engine пользуется под капотом для реального управления жизненным циклом контейнеров (запуск, остановка, управление образами) — сам Docker CLI/демон — более высокоуровневая обвязка поверх containerd с удобным UX (`docker build`, `docker compose` и т.п.). Docker понимает, что контейнер «умер», потому что containerd отслеживает главный процесс контейнера (PID 1 внутри контейнера) — как только этот процесс завершается (сам или из-за ошибки), containerd фиксирует это событие и помечает контейнер как `Exited`.
|
||||
|
||||
```bash
|
||||
docker inspect <container> --format '{{.State.Pid}}' # PID процесса контейнера на хосте
|
||||
ps aux | grep <тот же PID> # виден и на хосте — контейнер это просто изолированный процесс хоста
|
||||
```
|
||||
|
||||
### 7. Entrypoint vs CMD
|
||||
|
||||
```dockerfile
|
||||
ENTRYPOINT ["myapp"]
|
||||
CMD ["--config", "/etc/myapp/default.yml"]
|
||||
```
|
||||
|
||||
`ENTRYPOINT` задаёт неизменяемую (без явного `--entrypoint` при запуске) главную команду контейнера. `CMD` задаёт аргументы по умолчанию к ней, которые легко переопределить при `docker run myapp --config /other.yml` — эта строка заменит именно `CMD`, не трогая `ENTRYPOINT`. Если задан только `CMD` без `ENTRYPOINT` — вся строка `CMD` целиком заменяется аргументами `docker run`.
|
||||
|
||||
### 8. Копирование файлов в работающий контейнер
|
||||
|
||||
```bash
|
||||
docker cp ./local-file.txt <container>:/app/file.txt
|
||||
docker cp <container>:/app/output.log ./output.log
|
||||
```
|
||||
|
||||
## Что это даёт в разговоре с интервьюером
|
||||
|
||||
- Понимание, что контейнер технически — обычный процесс хоста с изоляцией через namespaces/cgroups, а не мини-VM.
|
||||
- Практика multi-stage build и осознанного слоёного кеширования, а не просто «Dockerfile работает».
|
||||
- Знание разницы bind mount/named volume не абстрактно, а с привязкой к тому, где какой тип реально применён в других кейсах репозитория.
|
||||
- Воспроизведённый вживую OOM kill (код 137) — понимание на практике, а не только в теории.
|
||||
|
||||
## Как это ложится в легенду
|
||||
|
||||
В реальной работе (АО ТНИИС) Docker упоминается в стеке отдела как средство контейнеризации сервисов сопровождения. Этот кейс показывает собственный опыт сборки, сетевого взаимодействия и диагностики контейнеров поверх уже готовых стендов мониторинга и nginx.
|
||||
58
stack/docker/QUESTIONS.md
Normal file
58
stack/docker/QUESTIONS.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Вопросы: Docker
|
||||
|
||||
Опираются на кейс: [CASE.md](CASE.md). Источники — [VK Cloud.md](../../interview/VK%20Cloud.md), [Реалист банк.md](../../interview/Реалист%20банк.md).
|
||||
|
||||
### В чём разница между образом (image) и контейнером?
|
||||
|
||||
Образ — неизменяемый шаблон: слоёная файловая система + метаданные (какая команда запускается по умолчанию, какие переменные окружения и т.д.), результат `docker build`. Контейнер — запущенный (или остановленный) экземпляр образа с добавленным поверх write-слоем, где живут любые runtime-изменения — по сути процесс на хосте плюс собственная изолированная файловая система, сеть и т.д. Один образ может породить много независимых контейнеров.
|
||||
|
||||
### В чём отличия виртуализации от контейнеризации?
|
||||
|
||||
Виртуализация эмулирует отдельное «железо» через гипервизор — у каждой VM своё полноценное ядро ОС, изоляция максимальная, но накладные расходы выше (полный загруженный образ ОС, медленнее старт). Контейнеризация — все контейнеры хоста делят одно и то же ядро хоста; изоляция достигается через namespaces (что процесс видит — свои PID, сеть, файловую систему) и ограничение ресурсов через cgroups (сколько он может использовать), см. [../linux-bash/QUESTIONS.md](../linux-bash/QUESTIONS.md). Контейнеры легче и стартуют за секунды, но изоляция менее строгая (общее ядро — потенциальная поверхность атаки при уязвимостях в ядре).
|
||||
|
||||
### Как Docker понимает, что контейнер умер, и помечает его как exit?
|
||||
|
||||
Внутри Docker Engine работает `containerd` — реальный рантайм, управляющий процессами контейнеров. Он отслеживает главный процесс контейнера (PID 1 внутри контейнерного namespace); как только этот процесс завершается (штатно или с ошибкой), это фиксируется как событие завершения, и статус контейнера меняется на `Exited` с соответствующим exit-кодом (`docker inspect --format '{{.State.ExitCode}}'`).
|
||||
|
||||
### Что такое Docker runtime и зачем нужен containerd?
|
||||
|
||||
Runtime — компонент, который непосредственно создаёт и управляет изолированными процессами контейнеров на уровне ОС (namespaces, cgroups, файловая система из слоёв образа). `containerd` — низкоуровневый рантайм, на котором построен сам Docker Engine: Docker CLI/демон — это удобная надстройка (сборка образов, compose, registry-взаимодействие) поверх `containerd`, который непосредственно исполняет работу по управлению жизненным циклом контейнеров. Это разделение позволяет другим системам (например, Kubernetes через CRI) использовать `containerd` напрямую, без полного Docker Engine.
|
||||
|
||||
### Как зайти внутрь контейнера?
|
||||
|
||||
```bash
|
||||
docker exec -it <container> sh # или bash, если есть в образе
|
||||
```
|
||||
|
||||
`exec` запускает новый процесс внутри уже работающего контейнера (в отличие от `docker run`, который создаёт новый контейнер). Если контейнер уже остановлен, `exec` не сработает — нужно либо запустить его заново, либо использовать `docker cp` для файлов без запуска процесса внутри.
|
||||
|
||||
### Как скопировать файл в работающий контейнер?
|
||||
|
||||
```bash
|
||||
docker cp ./local-file.txt <container>:/path/in/container
|
||||
docker cp <container>:/path/in/container ./local-file.txt # и в обратную сторону, из контейнера на хост
|
||||
```
|
||||
|
||||
### Если убить процесс Docker (сам демон) — запущенные контейнеры сдохнут?
|
||||
|
||||
Нет, не обязательно — сами контейнеры управляются `containerd`, а не напрямую демоном `dockerd`; при перезапуске `dockerd` уже запущенные контейнеры продолжают работать (это осознанное архитектурное решение, начиная с версии Docker, где Docker Engine был разделён с `containerd` — раньше, в старых версиях, где всё было завязано на монолитный демон, перезапуск демона действительно ронял контейнеры). После рестарта `dockerd` он заново подключается к уже работающим контейнерам через `containerd` и восстанавливает их видимость в `docker ps`.
|
||||
|
||||
### Как работает сеть в Docker?
|
||||
|
||||
По умолчанию Docker создаёт bridge-сеть (`docker0` для отдельных контейнеров, отдельная сеть на проект в Docker Compose), контейнеры получают виртуальные сетевые интерфейсы, подключённые к этому мосту, и приватные IP внутри него. Docker поднимает встроенный DNS внутри пользовательских (non-default) bridge-сетей — контейнеры резолвят друг друга по имени сервиса/контейнера, а не по IP. Наружу трафик пробрасывается через `-p host_port:container_port`, что настраивает NAT-правила (iptables) для перенаправления с порта хоста на IP контейнера внутри моста.
|
||||
|
||||
### Много ли процессов внутри одного контейнера? У контейнеров есть PID?
|
||||
|
||||
Технически контейнер может запускать сколько угодно процессов (например, если ENTRYPOINT — скрипт, который стартует несколько дочерних процессов), но общепринятая практика — один основной процесс на контейнер (принцип "один контейнер — одна ответственность"), чтобы жизненный цикл контейнера чётко соответствовал жизненному циклу этого процесса. У контейнера есть PID — как у любого обычного процесса хоста (см. `docker inspect --format '{{.State.Pid}}'` в кейсе), плюс внутри контейнера, благодаря PID namespace, главный процесс видит себя как PID 1 в своём изолированном пространстве.
|
||||
|
||||
### Entrypoint vs CMD — в чём разница?
|
||||
|
||||
`ENTRYPOINT` — фиксированная главная команда контейнера, `CMD` — аргументы к ней по умолчанию, которые можно переопределить, просто передав другие аргументы в `docker run image <новые аргументы>`. Если задан только `CMD` без `ENTRYPOINT`, вся команда целиком переопределяется тем, что передано в `docker run`. Практическая польза разделения — можно сделать образ, у которого фиксирована сама программа (`ENTRYPOINT ["myapp"]`), но легко менять флаги запуска без пересборки образа.
|
||||
|
||||
### Разница docker ps, docker ps -a, docker logs, docker images?
|
||||
|
||||
`docker ps` — только работающие контейнеры. `docker ps -a` — вообще все контейнеры, включая остановленные/завершившиеся. `docker logs <container>` — стандартный вывод (stdout/stderr) процесса внутри контейнера, накопленный с момента старта (`-f` — читать в реальном времени, как `tail -f`). `docker images` — список локально скачанных/собранных образов (в отличие от контейнеров — это шаблоны, а не запущенные экземпляры).
|
||||
|
||||
### Как сохранить логи из контейнера?
|
||||
|
||||
`docker logs <container> > output.log` — перенаправить stdout вывод команды в файл на хосте. Если логи пишутся не в stdout, а в файл внутри контейнера — `docker cp <container>:/path/to/log.file ./log.file`. Для постоянного сохранения логов за пределами жизни контейнера в проде обычно настраивают logging driver (например, `json-file` с ротацией, или отправку в централизованную систему — см. [../elk/CASE.md](../elk/CASE.md)) вместо ручного копирования.
|
||||
Reference in New Issue
Block a user