Питання на співбесіді з Docker
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
Мережевий драйвер визначає, як контейнер підключений до мережі хоста й до інших контейнерів.
bridge (за замовчуванням) - контейнер отримує власну мережеву «картку» у віртуальній мережі всередині хоста. Ззовні він недоступний, доки порт не опубліковано (-p 8080:80). Контейнери в одній bridge-мережі бачать один одного.
host - контейнер використовує мережу хоста напряму, без ізоляції:
docker run --network host nginx # nginx слухає порт 80 самого хоста
- плюс: немає накладних витрат на трансляцію адрес, корисно для програм з великим мережевим навантаженням чи багатьма портами;
- мінус: немає мережевої ізоляції, порти контейнера конфліктують з портами хоста,
-pігнорується. На Docker Desktop (macOS, Windows) працює з обмеженнями, бо «хост» - це віртуальна машина.
none - лише локальний інтерфейс (lo), жодної мережі. Для задач, яким мережа не потрібна й не повинна бути доступною: обробка файлів, генерація звітів з недовірених даних.
Інші драйвери:
overlay- мережа поверх кількох хостів (Docker Swarm), контейнери на різних серверах спілкуються як в одній мережі;macvlan/ipvlan- контейнер отримує власну адресу в фізичній мережі, як окремий пристрій. Для інтеграції зі старими системами, що очікують окремий IP;- плагіни сторонніх постачальників.
docker network ls
docker network create app-net
docker run --network app-net --name db postgres:18
Практичне правило: для застосунків - власні bridge-мережі (Compose створює їх автоматично). host - лише коли є виміряна потреба, бо він прибирає один із шарів ізоляції. none - для ізольованої обробки даних.
Публікація порту (-p) відкриває доступ до контейнера ззовні: трафік на порт хоста перенаправляється в контейнер. На відміну від EXPOSE у Dockerfile, який лише документує порт, -p справді змінює мережеві правила хоста.
docker run -p 8080:80 nginx # 0.0.0.0:8080 - на ВСІХ інтерфейсах хоста
docker run -p 127.0.0.1:8080:80 nginx # лише з самого хоста
docker run -p 10.0.0.5:8080:80 nginx # лише на конкретному інтерфейсі
Ключова різниця: без вказаної адреси порт публікується на всіх мережевих інтерфейсах (0.0.0.0 і ::) - тобто доступний з інтернету, якщо сервер має публічну адресу.
Типова аварія: на сервері в compose.yaml бази даних залишили з розробки:
services:
postgres:
ports:
- "5432:5432" # база доступна всьому інтернету
Сканери знаходять відкриті бази й Redis за хвилини. Через те, що Docker керує правилами фаєрвола сам, ufw таку публікацію не блокує.
Як правильно:
- сервісам, до яких звертаються лише інші контейнери (база, Redis, черги), - не публікувати порти взагалі: контейнери в одній мережі звертаються один до одного за іменем сервісу (
postgres:5432) без публікації; - доступ з хоста для розробки чи адміністрування -
127.0.0.1:5432:5432, а на сервері - SSH-тунель; - назовні - лише зворотний проксі (Nginx, Caddy, Traefik) на 80/443.
services:
postgres:
ports:
- "127.0.0.1:5432:5432"
Перевірити, що реально опубліковано: docker ps (колонка PORTS) чи ss -tlnp на хості.
Docker створює мережу з назвою bridge автоматично, і docker run без --network підключає контейнери саме до неї. Але для застосунків документація Docker радить власні (user-defined) bridge-мережі.
docker network create app-net
docker run -d --network app-net --name db postgres:18
docker run -d --network app-net --name web myapp
Відмінності:
1. DNS за іменами контейнерів. У власній мережі контейнер web звертається до бази як db:5432 - вбудований DNS Docker розв'язує ім'я в поточну IP-адресу. У мережі за замовчуванням імен немає - лише IP, які змінюються після перезапуску (застарілий --link для цього вже не рекомендований).
2. Ізоляція. Усі контейнери без явної мережі потрапляють в одну спільну мережу bridge і можуть «бачити» один одного. Власні мережі відокремлюють проєкти й групи сервісів: контейнер з однієї мережі не дістанеться до бази в іншій.
3. Підключення «на льоту». Контейнер можна приєднати до власної мережі чи від'єднати без перезапуску: docker network connect app-net web.
4. Налаштування (підмережа, MTU, внутрішня мережа без виходу в інтернет) задаються для кожної мережі окремо.
Docker Compose робить це автоматично: для проєкту створюється мережа <проєкт>_default, і сервіси звертаються один до одного за іменами сервісів. Тому в Laravel-проєкті з Compose у .env пишуть DB_HOST=mysql, а не IP.
Корисний прийом безпеки - кілька мереж:
services:
proxy:
networks: [frontend]
app:
networks: [frontend, backend]
db:
networks: [backend]
networks:
frontend:
backend:
internal: true # без виходу в інтернет
Зворотний проксі не має доступу до бази, а база не може сама звертатися в інтернет.
За замовчуванням процес у контейнері працює від root (UID 0). Перевірити просто: docker run --rm alpine id - покаже uid=0(root).
Чому це небезпечно, якщо контейнер ізольований? Ізоляція не абсолютна:
- root у контейнері - той самий root ядра, що й на хості (без user namespaces). Вразливість у ядрі чи середовищі виконання, помилкова конфігурація (
--privileged, змонтованийdocker.sock, змонтовані каталоги хоста) - і процес отримує права root на хості; - змонтовані каталоги: root у контейнері може змінювати й видаляти файли хоста в bind mount, створювати файли, які потім не видалиш без
sudo; - зламаний застосунок (RCE через вразливість у коді) отримує повні права всередині контейнера: встановити інструменти, змінити бінарні файли, читати всі файли.
Як запустити від звичайного користувача:
RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app
USER app
або під час запуску:
docker run --user 1000:1000 myapp
services:
app:
user: "1000:1000"
Що зазвичай ламається і як виправити:
- порти нижче 1024 - звичайний користувач їх не відкриє. Застосунок слухає 8080, а зовні публікується
-p 80:8080; - права на каталоги для запису (
storage,bootstrap/cacheу Laravel) -chownпри збиранні образу; - bind mount у розробці - UID у контейнері має збігатися з UID користувача на хості, інакше файли належатимуть «чужому» користувачу. Laravel Sail для цього використовує змінні
WWWUSER/WWWGROUP.
Офіційні образи: php-fpm запускає робочі процеси від www-data, хоча головний процес стартує від root; nginx має варіанти nginx-unprivileged. Для власних образів USER - стандартна практика, а сканери безпеки й Kubernetes-політики (runAsNonRoot) перевіряють це автоматично.
Віртуальна машина має власне ядро ОС, що працює поверх гіпервізора. Контейнер - звичайний процес хоста, який ділить ядро з хостом і з іншими контейнерами, але бачить обмежене «оточення».
Два головні механізми ядра Linux:
1. Простори імен (namespaces) - що процес бачить:
- PID - власне дерево процесів; процес контейнера має PID 1 всередині й не бачить процесів хоста;
- network - власні інтерфейси, адреси, таблиці маршрутизації, порти;
- mount - власна файлова система (образ + томи);
- UTS - власне ім'я хоста;
- IPC - власні черги повідомлень і спільна пам'ять;
- user (опційно) - відображення користувачів: root усередині може бути звичайним користувачем ззовні.
2. Контрольні групи (cgroups) - скільки ресурсів процес може використати: пам'ять, процесор, кількість процесів, ввід-вивід. Саме cgroups реалізують --memory, --cpus, --pids-limit.
Додаткові шари захисту: обмежені можливості (capabilities) root, профіль seccomp (заборонені системні виклики), AppArmor/SELinux.
Що з цього випливає:
- контейнери легкі: старт - мілісекунди, накладні витрати мінімальні, бо немає окремого ядра й гостьової ОС;
- ізоляція слабша, ніж у ВМ: вразливість у ядрі хоста потенційно доступна з будь-якого контейнера. Тому недовірений код (код користувачів, багатоорендні платформи) часто запускають у пісочницях з додатковою ізоляцією - gVisor, Kata Containers, Firecracker;
- ядро одне: Linux-контейнер не запуститься на ядрі Windows напряму. Docker Desktop на macOS і Windows запускає контейнери всередині легкої Linux-віртуальної машини;
- «контейнер - це межа безпеки» з обережністю: для довірених застосунків вона достатня, але не варто покладатися на неї як на єдиний захист.
Перевірити простори імен процесу: ls -l /proc/<pid>/ns на хості.
Docker Compose описує багатоконтейнерне оточення одним файлом: сервіси, мережі, томи, секрети. Одна команда docker compose up піднімає все разом.
# compose.yaml
services:
app:
build: .
ports:
- "127.0.0.1:8000:8000"
environment:
DB_HOST: postgres
depends_on:
postgres:
condition: service_healthy
volumes:
- .:/var/www/html
postgres:
image: postgres:18
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
volumes:
pgdata:
Основні розділи верхнього рівня:
services- контейнери з образом чи інструкціями збирання, портами, змінними, томами, залежностями;volumes- іменовані томи для даних, що мають переживати перестворення контейнерів;networks- мережі (за замовчуванням створюється одна для всього проєкту);secrets,configs- файли конфігурації й секретів;name- назва проєкту (префікс для контейнерів, мереж, томів).
Чому без version: колись файли починалися з version: "3.8", і версія визначала доступні можливості. Тепер Compose реалізує єдину Compose Specification - поле version застаріле й ігнорується. Сучасний Compose виводить попередження: «the attribute version is obsolete». Його можна просто видалити.
Назва файлу: рекомендована - compose.yaml (також підтримуються compose.yml і старі docker-compose.yml). Команда - docker compose (вбудований плагін, Compose v2), а не окрема програма docker-compose першої версії.
Корисні команди:
docker compose up -d # запустити у фоні
docker compose ps # стан сервісів
docker compose logs -f app # логи сервісу
docker compose down # зупинити й видалити контейнери й мережі (томи лишаються)
docker compose config # підсумкова конфігурація після підстановки змінних і злиття файлів
docker compose config - найкорисніша команда для налагодження: показує, що Compose реально «бачить».
Тут дві різні речі, які часто плутають: змінні для самого файлу Compose і змінні всередині контейнера.
1. Підстановка (interpolation) - Compose заміняє ${...} у compose.yaml до запуску контейнерів. Значення беруться з оточення shell і з файлу .env поруч із compose.yaml:
services:
app:
image: myapp:${APP_VERSION:-latest} # за замовчуванням latest
ports:
- "${APP_PORT:-8000}:8000"
environment:
DB_HOST: ${DB_HOST:?DB_HOST is required} # помилка, якщо не задано
${VAR:-default}- значення за замовчуванням, якщо змінна порожня чи не задана;${VAR:?повідомлення}- зупинити з помилкою, якщо не задана;$$- буквальний знак долара.
2. Змінні контейнера - потрапляють в оточення процесу всередині:
services:
app:
environment: # явно в compose.yaml
APP_ENV: local
env_file: # з файлу - усі змінні файлу йдуть у контейнер
- .env.docker
Ключова різниця: .env поруч із compose.yaml не передається в контейнер автоматично - він лише використовується для підстановки ${...}. А env_file передає змінні в контейнер, але не бере участі в підстановці.
Плутанина з Laravel: у Laravel-проєкті .env - це ще й файл конфігурації застосунку. Laravel Sail використовує це: той самий .env і для підстановки в compose.yaml (${APP_PORT}, ${FORWARD_DB_PORT}), і для застосунку, який читає його з каталогу проєкту через bind mount.
Пріоритет змінних контейнера (від вищого): docker compose run -e, environment у файлі, env_file, ENV в образі.
Перевірка: docker compose config показує файл після підстановки - видно, яке значення реально потрапило.
Безпека: .env з секретами - у .gitignore. Для продакшену секрети краще передавати механізмом секретів, а не змінними (їх видно в docker inspect).
Обидві команди виконують команду «в сервісі», але по-різному.
docker compose exec - виконує команду в уже запущеному контейнері сервісу:
docker compose exec app php artisan migrate
docker compose exec app sh # оболонка в працюючому контейнері
docker compose exec -u root app apk add htop # від іншого користувача
- контейнер має бути запущений;
- команда бачить той самий стан: файли, процеси, змінні оточення, з'єднання;
- після завершення контейнер продовжує працювати.
docker compose run - створює новий тимчасовий контейнер з конфігурації сервісу й виконує в ньому команду:
docker compose run --rm app composer install
docker compose run --rm app php artisan test
docker compose run --rm --no-deps node npm run build
- працює, навіть якщо сервіс не запущено;
- за замовчуванням запускає залежності (
depends_on) ---no-depsвимикає це; - не публікує порти сервісу (щоб не конфліктувати із запущеним), якщо не вказати
--service-ports; --rm- видалити контейнер після завершення, інакше накопичуються зупинені контейнери.
Коли що:
| Задача | Команда |
|---|---|
| міграції, tinker, черга в робочому оточенні | exec |
| подивитися, що відбувається в працюючому контейнері | exec |
| одноразова задача без запущеного сервісу (встановлення залежностей, тести в CI) | run --rm |
| команда в чистому оточенні, щоб не зачіпати запущений контейнер | run --rm |
Laravel Sail обгортає саме ці команди: sail artisan migrate - це docker compose exec laravel.test php artisan migrate, а sail shell - оболонка в працюючому контейнері.
Типова помилка: docker compose run app php artisan queue:work без --rm щодня - десятки забутих контейнерів. Подивитися їх: docker compose ps -a.
Коли щось не працює в Compose-оточенні, є стандартний порядок діагностики.
1. Стан сервісів:
docker compose ps -a
Колонки STATUS (Up, Exited (1), Restarting) і (healthy)/(unhealthy) одразу показують, який сервіс проблемний. Exited (137) - зазвичай вбито через нестачу пам'яті, Exited (1) - помилка застосунку.
2. Логи:
docker compose logs app # усі логи сервісу
docker compose logs -f --tail=100 app # останні 100 рядків і далі в реальному часі
docker compose logs --since 10m # усі сервіси за 10 хвилин
Видно лише те, що процес пише в stdout/stderr. Якщо Laravel пише в storage/logs/laravel.log, у docker compose logs цього не буде - для контейнерів краще LOG_CHANNEL=stderr.
3. Вхід у контейнер:
docker compose exec app sh
# усередині: перевірити файли, змінні, з'єднання
env | grep DB_
nc -zv postgres 5432
php artisan about
Якщо контейнер одразу падає і exec неможливий - запустити той самий образ з іншою командою:
docker compose run --rm --entrypoint sh app
4. Конфігурація й деталі:
docker compose config # що Compose реально застосовує
docker inspect $(docker compose ps -q app) # мережі, томи, змінні, healthcheck, причина зупинки
docker compose top # процеси в контейнерах
docker stats # пам'ять і CPU в реальному часі
Типові причини проблем і що перевірити:
- «Connection refused» до бази - використано
localhostзамість імені сервісу (DB_HOST=postgres), база ще не готова (healthcheck), сервіси в різних мережах; - зміни коду не видно - немає bind mount, кеш конфігурації Laravel (
php artisan optimize:clear), OPcache без перевірки часу змін; - права на файли - UID у контейнері не збігається з користувачем на хості;
- порт зайнятий - інший процес на хості вже слухає той самий порт;
- старий образ - після зміни
Dockerfileпотрібенdocker compose up --build.
Docker Desktop має графічний інтерфейс для логів, терміналу й файлів контейнера - зручно для швидкого огляду.
Laravel Sail - легкий інструмент для локальної розробки Laravel у Docker. По суті це файл compose.yaml у корені проєкту і скрипт sail, що спрощує команди Docker Compose. Окремої «магії» немає - це звичайний Compose.
Встановлення:
composer require laravel/sail --dev
php artisan sail:install # обрати сервіси: mysql, pgsql, redis, meilisearch, mailpit...
./vendor/bin/sail up -d
sail:install публікує compose.yaml і додає в .env змінні для підключення до сервісів у контейнерах. Додати сервіс пізніше - php artisan sail:add.
Скрипт sail - обгортка над docker compose:
| Sail | Що виконується |
|---|---|
sail up -d |
docker compose up -d |
sail artisan migrate |
php artisan migrate у контейнері застосунку |
sail composer require ... |
Composer у контейнері |
sail npm run dev |
Node у контейнері |
sail test |
тести в контейнері |
sail shell / sail root-shell |
оболонка в контейнері |
sail tinker |
Tinker |
Зручно зробити аліас: alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'.
Що варто знати:
- версія PHP обирається в
compose.yaml(образ на основіruntimes/8.5), підтримуються кілька версій; WWWUSER/WWWGROUP- UID користувача в контейнері відповідає користувачу хоста, щоб файли, створені в контейнері, не належали root;- налаштування образів -
sail artisan sail:publishкопіює Dockerfile-и в каталогdocker/для змін (розширення PHP, пакети); - Xdebug вмикається змінною
SAIL_XDEBUG_MODEу.env(develop,debug,coverage); - порти змінюються в
.env(APP_PORT,FORWARD_DB_PORT), якщо стандартні зайняті.
Чого Sail не робить: це інструмент розробки, а не продакшен-оточення. Образи Sail розраховані на зручність (вбудований сервер, інструменти, Node), а не на безпеку чи розмір. Для продакшену будують власні образи (наприклад, на FrankenPHP чи php-fpm + nginx) або використовують хостинг на кшталт Laravel Cloud.
Альтернативи: Laravel Herd (нативно на macOS/Windows без Docker), DDEV, власний compose.yaml.
docker run чи docker compose up запускає контейнери на одному сервері і на цьому зупиняється. У продакшені з'являються задачі, які ця команда не розв'язує:
- сервер упав - хто перезапустить контейнери на іншому сервері?
- навантаження зросло - як запустити ще п'ять копій застосунку і розподілити між ними трафік?
- новий реліз - як замінити контейнери по одному, щоб сайт не зупинявся, і відкотитися, якщо нова версія не стартує?
- контейнер «завис», але процес живий - хто це помітить і перезапустить?
- секрети й конфігурація - як доставити їх на всі сервери безпечно?
Оркестратор - система, якій описують бажаний стан («має працювати 4 копії образу app:a1b2c3, з такими ресурсами й перевірками стану»), а вона сама постійно підтримує його на кластері серверів:
- розподіляє контейнери по вузлах з урахуванням ресурсів;
- перезапускає й переносить їх при збоях;
- робить поступові оновлення й відкати;
- дає внутрішню мережу, балансування й пошук сервісів за іменем;
- керує секретами й конфігурацією.
Основні варіанти:
- Kubernetes - стандарт індустрії, величезна екосистема, але й складність: окремі знання, налаштування, супровід. Часто як керований сервіс (EKS, GKE, AKS);
- Docker Swarm - вбудований у Docker, значно простіший, підходить для невеликих кластерів;
- PaaS поверх Docker - Kamal, Coolify, Dokploy: деплой і оновлення без повноцінного оркестратора;
- керовані контейнерні сервіси - AWS ECS/Fargate, Google Cloud Run, Fly.io.
Чи потрібна оркестрація завжди: ні. Невеликий застосунок на одному-двох серверах чудово живе з Compose, політиками перезапуску й простим скриптом деплою. Kubernetes окупається, коли сервісів і серверів багато, а команда готова його підтримувати.
Політика перезапуску визначає, що Docker робить, коли процес контейнера завершився, і чи запускати контейнер після перезапуску самого Docker (наприклад, після перезавантаження сервера).
docker run -d --restart unless-stopped myapp:1.4
# compose.yaml
services:
app:
image: myapp:1.4
restart: unless-stopped
Варіанти:
| Політика | Поведінка |
|---|---|
no (за замовчуванням) |
не перезапускати ніколи |
on-failure[:N] |
лише якщо процес завершився з ненульовим кодом; :N - максимум спроб |
always |
перезапускати завжди; після перезапуску Docker - запустити знову, навіть якщо контейнер зупинили вручну |
unless-stopped |
як always, але якщо контейнер зупинили вручну (docker stop), після перезапуску Docker він лишиться зупиненим |
Різниця always і unless-stopped проявляється саме після перезавантаження сервера: вручну зупинений для обслуговування контейнер з always «воскресне», з unless-stopped - ні. Для більшості сервісів зручніший unless-stopped.
Що варто знати:
- перезапуск з наростаючою затримкою: якщо контейнер падає одразу після старту, Docker збільшує паузу між спробами (подвоюючи її), щоб не створювати навантаження нескінченним циклом. Затримка скидається, якщо контейнер пропрацював хоча б 10 секунд;
on-failureдоречний для задач, які мають завершитися (міграції, одноразові скрипти): успішне завершення з кодом 0 - не привід запускати знову;- політика перезапуску не перевіряє здоров'я: контейнер, що «завис», але процес живий, не буде перезапущений. Для цього потрібні перевірки стану (healthcheck) і оркестратор, який на них реагує;
- у Swarm і Kubernetes перезапуском керує сам оркестратор (
deploy.restart_policy,restartPolicyу pod), і він може перенести контейнер на інший вузол.
Типова помилка: без політики перезапуску після перезавантаження сервера (оновлення ядра, збій живлення) застосунок просто не підніметься, доки хтось не зайде й не запустить його вручну.
Тег - змінна мітка, що вказує на конкретний образ. latest - просто тег за замовчуванням, коли інший не вказано. Він не означає «найновіша версія»: це той образ, який останнім отримав цей тег.
Чому latest у продакшені - проблема:
- невідомо, що запущено:
myapp:latestвчора й сьогодні - різні образи. Розслідуючи інцидент, неможливо сказати, яка версія коду працювала; - неможливий надійний відкат: «повернутися на попередню версію» - на яку?
latestуже перезаписано; - різні сервери - різні версії: сервер, що завантажив образ раніше, і новий сервер після масштабування отримають різні образи під тим самим тегом;
- неявні оновлення: перезапуск контейнера чи новий вузол кластера раптом підтягує нову версію без деплою.
Як тегувати:
- незмінні теги з git SHA:
myapp:sha-a1b2c3d- однозначний зв'язок образу з комітом. Такий тег ніколи не перезаписується; - семантичні версії для релізів:
myapp:1.4.2, плюс за бажанням «плаваючі»myapp:1.4іmyapp:1для зручності; - кілька тегів на один образ - нормально: CI ставить і
sha-a1b2c3d, і1.4.2; latest- хіба що для локальної розробки чи як мітка «останньої збірки main», але не в конфігурації деплою.
docker build -t ghcr.io/acme/app:sha-a1b2c3d -t ghcr.io/acme/app:1.4.2 .
docker push --all-tags ghcr.io/acme/app
Ще надійніше - дайджест: ghcr.io/acme/app@sha256:... - адреса вмісту образу. На відміну від тегу, дайджест змінити неможливо: той самий дайджест завжди означає ті самі байти.
Те саме стосується базових образів у Dockerfile: FROM php:latest чи навіть FROM php:8.5-fpm може тихо змінитися між збираннями - версії варто фіксувати точніше.
Правило: у продакшені має бути можливо відповісти «який саме код зараз працює» і «як повернутися на попередню версію» за одну хвилину.
Реєстр образів (registry) - сервер, що зберігає образи й віддає їх за назвою й тегом. docker push завантажує образ у реєстр, docker pull - отримує.
docker push ghcr.io/acme/app:1.4.2
docker pull ghcr.io/acme/app:1.4.2
Повна назва образу містить реєстр: ghcr.io/acme/app. Без нього Docker звертається до Docker Hub (docker.io): php:8.5-fpm - це docker.io/library/php:8.5-fpm.
Популярні реєстри:
- Docker Hub - найбільший публічний, офіційні образи (
php,nginx,postgres); - GitHub Container Registry (
ghcr.io) - зручно разом з GitHub Actions, права доступу як у репозиторію; - хмарні: AWS ECR, Google Artifact Registry, Azure Container Registry - близько до серверів, з IAM-доступом;
- власні: Harbor, GitLab Registry, простий
registry:2.
Ліміти Docker Hub (за сторінкою документації «Usage and limits»):
| Користувач | Ліміт завантажень |
|---|---|
| без входу | 100 за 6 годин на IPv4-адресу (чи IPv6-підмережу /64) |
| Personal (з входом) | 200 за 6 годин |
| Pro, Team, Business | без ліміту (з урахуванням fair use) |
Чому це стосується продакшену й CI:
- CI без автентифікації ділить ліміт з усіма користувачами тієї самої IP-адреси (спільні раннери) - збирання раптом падають з помилкою ліміту;
- кластер з багатьма вузлами, що одночасно завантажують базові образи, теж швидко вичерпує ліміт однієї публічної IP.
Що робити:
docker loginу CI й на серверах - навіть безкоштовний обліковий запис подвоює ліміт;- власний реєстр для своїх образів (GHCR, ECR) і дзеркало/кеш для базових образів (pull-through cache);
- не завантажувати зайве: кеш шарів на раннерах, однакові базові образи.
Безпека: приватні образи містять код застосунку - доступ до реєстру з окремими токенами лише на читання для серверів і на запис лише для CI.
Докладніше в документації: Docker Hub: використання й ліміти
Усе, що контейнер пише в stdout і stderr, Docker за замовчуванням зберігає у файли на диску сервера - драйвер json-file (/var/lib/docker/containers/<id>/<id>-json.log).
Проблема: у json-file за замовчуванням немає обмеження розміру (max-size = необмежено). Застосунок, що активно логує, за кілька тижнів чи місяців заповнює диск - і сервер падає разом з базою, яка не може писати.
Ротація для всього сервера - /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Кожен контейнер тримає щонайбільше 3 файли по 10 МБ; старіші видаляються. Значення в log-opts мають бути рядками ("3", а не 3).
Важливо: нові налаштування daemon.json застосовуються лише до нових контейнерів. Наявні треба перестворити (docker compose up -d --force-recreate), інакше вони логуватимуть по-старому.
Для окремого сервісу - у Compose:
services:
app:
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
Альтернатива - драйвер local: зберігає логи компактніше (стиснення) і за замовчуванням має ротацію. docker logs з ним працює так само.
Як знайти винуватця:
sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail
docker system df # скільки займають образи, контейнери, томи, кеш збирання
Інші «пожирачі» диска на Docker-сервері: старі образи після кожного деплою, кеш збирання, зупинені контейнери, «осиротілі» томи. Їх прибирають docker image prune, docker builder prune - обережно й розуміючи, що видаляється (особливо docker volume prune, що видаляє дані).
Довгострокове рішення: застосунок пише в stdout, а логи збирає й відправляє централізована система (Loki, ELK, хмарний сервіс). Локальні файли - лише короткий буфер з ротацією.
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії