Уроки D2-D7 (14 файлов): деревья/куча, epoll, потоки, gdb, графы, L2-L3, ДП, TCP, bash, ядро, docker, мок-интервью + cards_src 613
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
# D6, часть 2. Docker минимум под сборку и тесты (урок)
|
||||
|
||||
Это не про эксплуатацию продакшн-кластеров, а про Docker как инструмент воспроизводимой
|
||||
сборки и тестового окружения для C++: одна и та же среда сборки — компилятор, тулчейн,
|
||||
библиотеки — у каждого разработчика и на CI, вместо «у меня собирается, у тебя нет».
|
||||
|
||||
## 1. Контейнер — это процесс, а не виртуальная машина
|
||||
|
||||
Контейнер — обычный процесс хостовой ОС, изолированный двумя независимыми механизмами ядра
|
||||
Linux:
|
||||
- **namespaces** — отдельное пространство имён для каждого вида ресурса: **pid** (свой список
|
||||
процессов, процесс внутри контейнера видит себя как PID 1), **mnt** (своя точка монтирования
|
||||
файловой системы), **net** (свой сетевой стек — интерфейсы, адреса, таблица маршрутизации),
|
||||
**uts** (свой hostname), **ipc** (изолированные механизмы межпроцессного взаимодействия —
|
||||
очереди сообщений, семафоры), **user** (отображение UID/GID контейнера на другие UID/GID
|
||||
хоста).
|
||||
- **cgroups** (control groups) — ограничение и учёт потребления ресурсов: сколько CPU, памяти,
|
||||
дискового ввода-вывода разрешено процессу и его потомкам.
|
||||
|
||||
Ключевое отличие от виртуальной машины: контейнеру не нужно собственное ядро — он использует
|
||||
ядро хоста напрямую, тогда как VM через гипервизор эмулирует виртуальное железо, поверх
|
||||
которого грузится отдельное ядро гостевой ОС. Отсюда напрямую следуют два практических
|
||||
эффекта: старт контейнера — это по сути `fork`/`exec` с применёнными namespaces, то есть
|
||||
доли секунды и накладные расходы, близкие к нулю (VM грузит собственное ядро — секунды и
|
||||
проценты CPU/памяти на гипервизор); и системные вызовы контейнера выполняет то же самое
|
||||
ядро хоста без эмуляции, то есть без потери производительности на виртуализацию — но и без
|
||||
изоляции на уровне ядра: уязвимость ядра или неверно настроенные capabilities способны дать
|
||||
выход из контейнера на хост, чего с отдельным ядром VM добиться сложнее.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Из каких двух механизмов ядра Linux состоит изоляция контейнера? — namespaces (изоляция видимости ресурсов) и cgroups (ограничение потребления ресурсов)
|
||||
- base | Перечисли namespaces, разбираемые в этом разделе? — pid, mnt, net, uts, ipc, user
|
||||
- core | Почему контейнер стартует за доли секунды, а VM — за секунды? — контейнер не грузит собственное ядро, старт — это fork/exec с применёнными namespaces; VM грузит через гипервизор целое гостевое ядро
|
||||
- core | Почему изоляция контейнера слабее, чем у VM, на уровне безопасности? — контейнер и хост используют одно и то же ядро; уязвимость ядра или неверные capabilities могут дать выход на хост, а у VM с отдельным ядром такой прямой путь отсутствует
|
||||
|
||||
Почему дальше: контейнер запускается из образа — разберём, из чего состоит сам образ и почему
|
||||
порядок команд при его сборке влияет на скорость пересборки.
|
||||
|
||||
## 2. Слои образа и кэш сборки
|
||||
|
||||
Образ Docker — неизменяемый набор **слоёв**: каждая инструкция Dockerfile (`RUN`, `COPY`)
|
||||
порождает отдельный слой — diff файловой системы относительно предыдущего слоя, слои
|
||||
read-only и кэшируются по хешу содержимого. При сборке Docker идёт по инструкциям сверху вниз
|
||||
и для каждой проверяет кэш: если инструкция и её входные данные не изменились с прошлой
|
||||
сборки (для `COPY` — содержимое копируемых файлов), слой берётся из кэша без выполнения; как
|
||||
только один слой не совпал с кэшем, **все последующие слои пересобираются заново**, даже если
|
||||
сами по себе не менялись — кэш линеен и рвётся в первой же точке расхождения.
|
||||
|
||||
Отсюда практическое правило порядка инструкций: сначала копировать и устанавливать
|
||||
зависимости (меняются редко), и только потом копировать исходный код (меняется на каждом
|
||||
коммите). Если сделать наоборот — любая правка одной строки кода инвалидирует кэш
|
||||
зависимостей, и сборка каждый раз заново качает пакеты из сети, что на CI ощутимо по времени.
|
||||
|
||||
**Ловушки**
|
||||
- `COPY . .` перед установкой зависимостей → любое изменение кода инвалидирует и слой с
|
||||
зависимостями → пересборка образа качает все пакеты заново на каждый коммит, видно по
|
||||
резко выросшему времени сборки в CI.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что порождает каждая инструкция `RUN`/`COPY` в Dockerfile? — отдельный слой (diff файловой системы)
|
||||
- core | Что происходит с последующими слоями, если один слой не совпал с кэшем? — все последующие слои пересобираются заново, даже если сами по себе не менялись
|
||||
- core | Какой порядок инструкций Dockerfile правильный для скорости пересборки? — сначала зависимости (меняются редко), потом исходный код (меняется часто)
|
||||
|
||||
Почему дальше: применим это правило к конкретному Dockerfile для сборки C++-проекта.
|
||||
|
||||
## 3. Dockerfile для C++ сборки
|
||||
|
||||
```dockerfile
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential cmake git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
COPY CMakeLists.txt .
|
||||
COPY src/ src/
|
||||
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
|
||||
```
|
||||
|
||||
Установку пакетов делают **одной инструкцией `RUN`** (`apt-get update && apt-get install ...
|
||||
&& rm -rf /var/lib/apt/lists/*`), а не отдельными командами — потому что слой фиксирует
|
||||
файловую систему на момент завершения именно этой инструкции. Если `rm -rf
|
||||
/var/lib/apt/lists/*` вынести в отдельный `RUN` после установки, скачанный кеш списков
|
||||
пакетов уже необратимо запечён в предыдущем слое и продолжает занимать место в итоговом
|
||||
образе — слой нельзя «похудеть» задним числом последующим слоем, можно только скрыть файл в
|
||||
новом слое поверх старого.
|
||||
|
||||
`WORKDIR` задаёт и фиксирует рабочую директорию для всех последующих инструкций; `COPY`
|
||||
переносит файлы с хоста в образ отдельным слоем — именно поэтому в примере сначала копируется
|
||||
`CMakeLists.txt`, а исходники — вторым `COPY`: правка кода не трогает слой с конфигурацией
|
||||
сборки. Каталог `build/`, уже собранный на хосте разработчика, копировать в образ **нельзя**:
|
||||
он собран под окружение хоста (другая версия компилятора, другие пути, возможно другая
|
||||
архитектура) и не гарантированно совместим с окружением внутри контейнера — сборка должна
|
||||
проходить внутри самого образа, чтобы результат был воспроизводим одинаково у всех.
|
||||
|
||||
**Ловушки**
|
||||
- `apt-get install` и `rm -rf /var/lib/apt/lists/*` в разных `RUN` → кеш списков пакетов
|
||||
необратимо остаётся в промежуточном слое → итоговый образ ощутимо больше, чем при
|
||||
объединении в одну инструкцию, видно по `docker history`.
|
||||
- Копирование готового `build/` с хоста в образ вместо сборки внутри контейнера → бинарник
|
||||
собран под окружение хоста, а не образа → несовместимость версий библиотек или архитектуры,
|
||||
«works on my machine» переносится прямо в контейнер.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Почему `apt-get install` и `rm -rf /var/lib/apt/lists/*` объединяют в одну инструкцию `RUN`? — слой фиксирует файловую систему на момент завершения инструкции; в отдельном RUN кеш пакетов уже необратимо запечён в предыдущем слое
|
||||
- core | Почему нельзя копировать в образ каталог `build/`, собранный на хосте? — он собран под окружение хоста (версия компилятора, пути, архитектура), не гарантированно совместим с окружением контейнера; сборка должна идти внутри образа
|
||||
|
||||
Почему дальше: если собирать всё в одном образе, финальный образ несёт в себе весь тулчейн
|
||||
сборки (компилятор, cmake, заголовки) — для рантайма это лишний вес и лишняя поверхность
|
||||
атаки. Разберём, как это разделить.
|
||||
|
||||
## 4. Multi-stage build
|
||||
|
||||
```dockerfile
|
||||
FROM debian:bookworm AS builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential cmake \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
COPY --from=builder /src/build/app /usr/local/bin/app
|
||||
ENTRYPOINT ["/usr/local/bin/app"]
|
||||
```
|
||||
|
||||
Первый `FROM ... AS builder` — образ со всем тулчейном сборки; второй `FROM` начинает
|
||||
**новый, независимый** образ, в который командой `COPY --from=builder` переносится только
|
||||
готовый результат — скомпилированный бинарник. Слои со всем тулчейном сборки в финальный
|
||||
образ не попадают вовсе. Итоговый рантайм-образ для этого выбирают минимальным:
|
||||
`debian:bookworm-slim` (урезанный Debian с базовыми библиотеками) — если бинарник собран
|
||||
динамически и нужна libc; либо **`scratch`** (полностью пустой образ, без единого файла) —
|
||||
подходит только для полностью статически слинкованного бинарника, которому вообще не нужна
|
||||
никакая библиотека окружения.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что переносит `COPY --from=builder` во второй `FROM`? — только указанные готовые файлы (например бинарник) из первого этапа, без слоёв тулчейна
|
||||
- core | Когда для финального этапа multi-stage можно использовать `FROM scratch`? — когда бинарник собран полностью статически и не нуждается ни в одной библиотеке окружения
|
||||
- core | Чем `debian:bookworm-slim` в качестве финального образа лучше полного `debian:bookworm` для рантайма? — не несёт тулчейн сборки и лишние пакеты, меньше размер и меньше поверхность атаки
|
||||
|
||||
Почему дальше: образ — это шаблон файловой системы, но данные, которые должны пережить
|
||||
пересоздание контейнера (например, база данных теста), нельзя держать в самом
|
||||
read-write-слое контейнера — для этого есть volume и bind mount.
|
||||
|
||||
## 5. Volume и bind mount
|
||||
|
||||
Оба механизма монтируют в контейнер хранилище, которое живёт отдельно от read-write-слоя
|
||||
контейнера (тот слой стирается командой `docker rm`).
|
||||
- **Volume** — область, которой управляет сам Docker, физически хранится в его служебной
|
||||
директории на хосте, не привязана к конкретному пути на диске разработчика — переносима
|
||||
между машинами, подходит для персистентных данных вроде базы данных теста.
|
||||
- **Bind mount** — прямое монтирование конкретного каталога хоста внутрь контейнера по
|
||||
заданному пути: контейнер и хост видят один и тот же каталог одновременно и правки на хосте
|
||||
сразу видны внутри без пересборки образа — используется при разработке, когда исходники
|
||||
редактируются на хосте, а собираются/тестируются внутри контейнера.
|
||||
|
||||
**Ловушки**
|
||||
- Держать данные в обычном read-write-слое контейнера без volume → `docker rm` или
|
||||
пересоздание контейнера безвозвратно стирает данные → «тесты вчера прошли, база сегодня
|
||||
пустая» без единой ошибки при удалении.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Кто физически управляет расположением volume на диске? — сам Docker, служебная директория, не путь, выбранный вручную
|
||||
- core | Почему bind mount, а не volume, используют для разработки с редактированием кода на хосте? — bind mount даёт прямой одновременный доступ к каталогу хоста, правки на хосте сразу видны в контейнере без пересборки образа
|
||||
|
||||
Почему дальше: то, как контейнер запускается и как к нему обращаются после старта, задаётся
|
||||
флагами `docker run` и парой соседних команд — разберём их.
|
||||
|
||||
## 6. `docker run`, `docker exec`, `docker logs`
|
||||
|
||||
Частые флаги `docker run`:
|
||||
- **`--rm`** — автоматически удалить контейнер (его read-write-слой) сразу после завершения
|
||||
процесса; без него остановленные контейнеры копятся и занимают место на диске.
|
||||
- **`-v host_path:container_path`** (или `-v volume_name:container_path`) — bind mount или
|
||||
named volume (раздел 5).
|
||||
- **`-p host_port:container_port`** — пробросить порт с хоста на порт внутри контейнера
|
||||
(контейнер по умолчанию в изолированной bridge-сети не виден снаружи без явного проброса).
|
||||
- **`--network=host`** — контейнер использует сетевой стек хоста напрямую, без собственного
|
||||
сетевого namespace и без проброса портов, но и без сетевой изоляции.
|
||||
|
||||
**`docker exec <container> <cmd>`** запускает дополнительную команду внутри уже работающего
|
||||
контейнера — типичный случай: открыть интерактивный shell (`docker exec -it <container>
|
||||
bash`) для отладки живого процесса без его остановки. **`docker logs <container>`** показывает
|
||||
вывод stdout/stderr, который написал процесс с PID 1 внутри контейнера, — то, что видно в
|
||||
терминале при `docker run` без `-d`, доступно так же и после отсоединения.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что делает флаг `--rm` у `docker run`? — автоматически удаляет контейнер и его read-write-слой сразу после завершения процесса
|
||||
- base | Какая команда даёт интерактивный shell в уже запущенном контейнере? — `docker exec -it <container> bash`
|
||||
- core | Чем `--network=host` отличается от обычного режима с `-p`? — контейнер напрямую использует сетевой стек хоста без собственного network namespace и без проброса портов, но и без сетевой изоляции
|
||||
|
||||
Почему дальше: реальная задача редко исчерпывается одним контейнером — обычно нужен ещё
|
||||
тестовый брокер, база данных или несколько сервисов сразу; управлять их сетью и порядком
|
||||
запуска вручную неудобно — для этого docker compose.
|
||||
|
||||
## 7. `docker compose`
|
||||
|
||||
`docker-compose.yml` — декларативное описание нескольких связанных контейнеров как одного
|
||||
приложения: сервисы с указанием образа или пути к Dockerfile, портов, volume, переменных
|
||||
окружения и зависимостей между сервисами. `docker compose up -d` поднимает все сервисы разом
|
||||
и автоматически создаёт для них общую сеть, где сервисы видят друг друга по имени из YAML как
|
||||
по hostname — не нужно вручную создавать сеть и связывать контейнеры по IP-адресам, которые
|
||||
могут меняться при пересоздании.
|
||||
|
||||
`docker compose down` без флага `-v` останавливает и удаляет контейнеры, но **сохраняет
|
||||
именованные volume**; `-v` удаляет и их. Типичная ошибка — предположить, что обычный `down`
|
||||
стирает данные тестовой базы (нет, если она в именованном volume), либо наоборот случайно
|
||||
потерять данные, добавив `-v` не задумавшись.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какая команда поднимает все сервисы из `docker-compose.yml` разом? — `docker compose up -d`
|
||||
- core | Что удаляет `docker compose down -v`, чего не удаляет `docker compose down` без флага? — именованные volume и данные в них
|
||||
|
||||
Почему дальше: сервисы в compose ссылаются на образы по имени и тегу — разберём, что означает
|
||||
тег и почему один из них считается плохой практикой.
|
||||
|
||||
## 8. Реестры и теги
|
||||
|
||||
Образ идентифицируется именем и тегом (`myapp:1.4.0`); реестр (например Docker Hub или
|
||||
приватный registry) хранит образы, `docker pull`/`docker push` их скачивают/загружают.
|
||||
Тег **`:latest`** — не «самая новая версия» в смысле гарантии, а обычный мутируемый тег,
|
||||
который каждый `docker push` без явного тега перезаписывает: `myapp:latest`, скачанный сегодня
|
||||
и через месяц, может указывать на совершенно разное содержимое образа. Это ломает
|
||||
воспроизводимость сборки и тестов — CI, зафиксировавший `myapp:latest`, через месяц может
|
||||
неожиданно тянуть другой код без единой изменённой строчки в собственном конфиге. Практика —
|
||||
фиксировать конкретную версию тега или дайджест образа (`myapp@sha256:...`), который
|
||||
неизменяем по определению хеша.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что физически происходит с тегом `:latest` при каждом `docker push` без явного тега? — он перезаписывается на новый образ, становится мутируемым указателем, а не фиксированной версией
|
||||
- core | Почему фиксация `myapp@sha256:...` вместо тега `:latest` важна для воспроизводимости CI? — дайджест неизменяем по определению хеша, а тег `:latest` может незаметно указывать на другое содержимое образа в разное время
|
||||
|
||||
Почему дальше: помимо версии образа, есть ещё вопрос — от чьего имени процесс исполняется
|
||||
внутри контейнера, и почему это не всё равно.
|
||||
|
||||
## 9. Права: `--user` и почему root в контейнере опасен
|
||||
|
||||
По умолчанию процесс внутри контейнера, если не указано иное, запускается от **root**
|
||||
(UID 0) — того же UID 0, что и root на хосте, если не настроен user namespace с ремаппингом
|
||||
UID. Раз у контейнера нет отдельного ядра (раздел 1), эскалация из контейнерного root до
|
||||
root на хосте — через уязвимость ядра, неверно выданные capabilities или смонтированный внутрь
|
||||
`docker.sock` — гораздо ближе и реальнее, чем аналогичный побег из виртуальной машины с
|
||||
отдельным ядром.
|
||||
|
||||
Флаг **`--user uid:gid`** запускает процесс контейнера с непривилегированным UID/GID вместо
|
||||
root — снижает ущерб от компрометации процесса внутри контейнера: даже получив контроль над
|
||||
процессом, атакующий не имеет привилегий root ни внутри контейнера, ни тем более на хосте.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | От какого пользователя запускается процесс в контейнере по умолчанию, если не указано иное? — root (UID 0)
|
||||
- core | Почему root в контейнере опаснее, чем root в отдельной VM? — контейнер не имеет отдельного ядра; эскалация до root хоста возможна через уязвимость общего ядра, неверные capabilities или смонтированный docker.sock, чего с отдельным ядром VM добиться сложнее
|
||||
- core | Что делает флаг `--user uid:gid`? — запускает процесс контейнера от непривилегированного UID/GID вместо root
|
||||
|
||||
Почему дальше: помимо прав, отдельный вопрос — сколько ресурсов хоста контейнеру вообще
|
||||
разрешено потреблять, чтобы один тестовый контейнер не положил всю машину.
|
||||
|
||||
## 10. Ограничения ресурсов: `--memory`, `--cpus`
|
||||
|
||||
- **`--memory=512m`** — жёсткий лимит памяти через cgroups; при превышении лимита OOM-killer
|
||||
убивает процесс контейнера сигналом `SIGKILL` — тот же механизм и тот же код завершения
|
||||
**137 = 128 + 9** (128 — соглашение shell/wait о сигнальном завершении, 9 — номер SIGKILL),
|
||||
что и при обычном `kill -9` вне контейнера, но здесь его вызывает превышение
|
||||
cgroup-лимита, а не человек.
|
||||
- **`--cpus=1.5`** — ограничение через CPU-контроллер cgroups: контейнер не может использовать
|
||||
больше эквивалента 1.5 ядра процессорного времени, даже если на хосте простаивают
|
||||
дополнительные ядра.
|
||||
|
||||
Ограничения задаются той же cgroups-инфраструктурой, что обеспечивает саму изоляцию
|
||||
контейнера (раздел 1) — это не отдельный механизм Docker, а применение уже существующего
|
||||
механизма ядра с конкретными числами.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Каким сигналом и с каким кодом завершения убивает процесс превышение лимита `--memory`? — SIGKILL, код завершения 137 (128 + 9)
|
||||
- core | Что ограничивает `--cpus=1.5` технически? — квоту CPU-контроллера cgroups, эквивалент 1.5 ядра процессорного времени вне зависимости от простаивающих ядер хоста
|
||||
|
||||
Ссылки на задачи этого набора: `tasks/06_threads` — типичный кандидат на тестирование внутри
|
||||
контейнера с ограничением `--cpus`, чтобы гонки данных проявлялись стабильнее под реальным
|
||||
давлением на планировщик; `tasks/09_gdb` — отладка бинарника внутри контейнера требует флага
|
||||
`--cap-add=SYS_PTRACE` (по умолчанию Docker урезает capabilities, и `ptrace`, на котором
|
||||
работает `gdb`/`strace`, без этого флага запрещён).
|
||||
|
||||
<details>
|
||||
<summary>Проверь себя</summary>
|
||||
|
||||
1. В Dockerfile сначала `COPY . .` копирует весь исходный код, а уже потом идёт установка
|
||||
зависимостей через `apt-get`. Что произойдёт со временем пересборки при правке одной
|
||||
строки кода и почему?
|
||||
<details><summary>Ответ</summary>Слой с `COPY . .` изменится при любой правке кода, и по
|
||||
правилу линейного кэша все последующие слои — включая установку зависимостей —
|
||||
пересоберутся заново, заново скачивая пакеты из сети. Правильный порядок — сначала
|
||||
зависимости (меняются редко), потом код.</details>
|
||||
|
||||
2. Финальный образ после multi-stage build весит существенно меньше, чем промежуточный
|
||||
`builder`-образ, хотя бинарник тот же самый. За счёт чего?
|
||||
<details><summary>Ответ</summary>Второй `FROM` начинает независимый образ, в который
|
||||
`COPY --from=builder` переносит только сам готовый бинарник — весь тулчейн сборки
|
||||
(компилятор, cmake, заголовки и их слои) остаётся только в промежуточном `builder`-образе
|
||||
и в финальный не попадает.</details>
|
||||
|
||||
3. Тестовый контейнер убит с кодом завершения 137 после превышения лимита `--memory=256m`.
|
||||
Что произошло механически и с чем ещё встречается тот же код завершения вне контейнеров?
|
||||
<details><summary>Ответ</summary>cgroups-контроллер памяти зафиксировал превышение лимита
|
||||
и OOM-killer прислал процессу `SIGKILL` (сигнал 9); shell/wait-конвенция кодирует
|
||||
завершение по сигналу как 128 + номер сигнала = 137. Тот же код 137 виден при обычном
|
||||
`kill -9` процесса вне всякого контейнера — механизм кодирования тот же.</details>
|
||||
|
||||
4. Почему root внутри контейнера — больший риск, чем root внутри виртуальной машины,
|
||||
изолирующей тот же процесс?
|
||||
<details><summary>Ответ</summary>Контейнер не имеет собственного ядра — он использует ядро
|
||||
хоста напрямую, поэтому уязвимость в этом общем ядре, неверно выданные capabilities или
|
||||
смонтированный `docker.sock` могут дать выход из контейнерного root прямо в root хоста.
|
||||
VM с отдельным гостевым ядром не даёт такого прямого пути.</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Материалы
|
||||
|
||||
- docs.docker.com, Dockerfile reference — https://docs.docker.com/engine/reference/builder/
|
||||
- docs.docker.com, Multi-stage builds — https://docs.docker.com/build/building/multi-stage/
|
||||
- docs.docker.com, `docker run` CLI reference — https://docs.docker.com/engine/reference/commandline/run/
|
||||
- docs.docker.com, Volumes — https://docs.docker.com/storage/volumes/
|
||||
- docs.docker.com, Compose file reference — https://docs.docker.com/compose/compose-file/
|
||||
- man7.org, `namespaces(7)` — https://man7.org/linux/man-pages/man7/namespaces.7.html
|
||||
- man7.org, `cgroups(7)` — https://man7.org/linux/man-pages/man7/cgroups.7.html
|
||||
Reference in New Issue
Block a user