Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді з 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 - для ізольованої обробки даних.

Докладніше в документації: Мережеві драйвери Docker

Публікація порту (-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   # без виходу в інтернет

Зворотний проксі не має доступу до бази, а база не може сама звертатися в інтернет.

Докладніше в документації: Драйвер bridge

За замовчуванням процес у контейнері працює від 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 Engine

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

Тут дві різні речі, які часто плутають: змінні для самого файлу 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).

Докладніше в документації: Підстановка змінних у Compose

Обидві команди виконують команду «в сервісі», але по-різному.

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.

Докладніше в документації: docker compose run

Коли щось не працює в 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 має графічний інтерфейс для логів, терміналу й файлів контейнера - зручно для швидкого огляду.

Докладніше в документації: docker compose logs

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.

Докладніше в документації: Laravel Sail

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 окупається, коли сервісів і серверів багато, а команда готова його підтримувати.

Докладніше в документації: 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 може тихо змінитися між збираннями - версії варто фіксувати точніше.

Правило: у продакшені має бути можливо відповісти «який саме код зараз працює» і «як повернутися на попередню версію» за одну хвилину.

Докладніше в документації: docker image tag

Реєстр образів (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, хмарний сервіс). Локальні файли - лише короткий буфер з ротацією.

Докладніше в документації: Драйвер логів json-file

Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 35 Senior 30

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії