Docker для веброзробника: від першого запуску до продакшну

Docker 4 лип. 2026 р.
 Docker для веброзробника: від першого запуску до продакшну

Docker давно перестав бути іграшкою для DevOps-ентузіастів — сьогодні це базовий інструмент будь-якого веброзробника. Він розв'язує вічну проблему «на моїй машині працює»: застосунок разом із усіма залежностями пакується в контейнер, який однаково запуститься на ноутбуку, тестовому й бойовому сервері.

Ця стаття — практичний шлях від першого docker run до готового до продакшну розгортання. Без глибокої теорії про cgroups і namespaces — тільки те, що реально знадобиться, щоб підняти й обслуговувати свій сайт у контейнерах.

Встановлення

На Ubuntu/Debian ставимо офіційний Docker Engine (не docker.io зі стандартних репозиторіїв — він застарілий):

curl -fsSL https://get.docker.com | sh

# Щоб не писати sudo перед кожною командою
sudo usermod -aG docker $USER
newgrp docker

# Перевірка
docker run hello-world

Сучасний Docker уже містить плагін Compose, тож окремо його ставити не треба. Перевірка: docker compose version (саме через пробіл, стара команда docker-compose з дефісом — deprecated).

Крок 1: перший контейнер

Найкоротший спосіб відчути Docker — підняти nginx однією командою:

docker run --rm -p 8080:80 nginx

Розбираємо по частинах:

  • run — запустити контейнер із образу;
  • --rm — видалити контейнер після зупинки (щоб не сміттярити);
  • -p 8080:80 — прокинути порт хоста 8080 → на порт 80 усередині контейнера;
  • nginx — образ із Docker Hub (завантажиться автоматично).

Відкриваємо http://localhost:8080 — бачимо привітальну сторінку nginx. Зупиняємо через Ctrl+C. Вітаю, ви щойно запустили вебсервер, не встановивши його в систему.

Крок 2: образи, контейнери, том — ключові поняття

Щоб не плутатись далі, розкладемо три сутності:

  • Образ (image) — незмінний шаблон, «зліпок» файлової системи із застосунком. Аналог класу в ООП.
  • Контейнер (container) — запущений екземпляр образу. Аналог об'єкта. Контейнерів з одного образу можна підняти скільки завгодно.
  • Том (volume) — постійне сховище, яке переживає перезапуск і видалення контейнера. Усе, що всередині контейнера, при його видаленні зникає; усе, що в томі, — лишається.

Базові команди для орієнтування:

docker ps              # запущені контейнери
docker ps -a           # усі, включно зі зупиненими
docker images          # локальні образи
docker logs -f <імʼя>   # логи контейнера в реальному часі
docker exec -it <імʼя> bash   # зайти всередину контейнера
docker stats           # споживання CPU/RAM у реальному часі

Крок 3: від run до docker-compose

Довгі команди docker run з десятком прапорців незручно набирати руками й неможливо документувати. Тому в реальній роботі майже завжди використовують Docker Compose — описуємо весь стек в одному YAML-файлі.

Приклад: типовий стек для PHP-сайту — веб, PHP-FPM і база. Файл docker-compose.yml:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./src:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php

  php:
    image: php:8.3-fpm-alpine
    volumes:
      - ./src:/var/www/html

  db:
    image: mariadb:11
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: mysite
      MYSQL_USER: mysite
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  dbdata:

Запуск усього стеку однією командою:

docker compose up -d      # -d = у фоновому режимі
docker compose logs -f    # логи всіх сервісів
docker compose down       # зупинити й видалити контейнери

Зверніть увагу на два типи volumes у прикладі:

  • Bind mount (./src:/var/www/html) — прив'язка каталогу з хоста. Ідеально для розробки: правите файл локально — зміни одразу в контейнері.
  • Named volume (dbdata) — керований Docker'ом том для даних бази. Переживає docker compose down, дані не губляться.

Docker Compose поглиблено

Базового up -d вистачає для старту, але справжня сила Compose розкривається в кількох речах, які варто знати з самого початку.

Мережі та зв'язок між сервісами

Головне непорозуміння новачків: «як контейнер бачить базу?». Відповідь — Compose автоматично створює спільну мережу для всіх сервісів стеку, і кожен сервіс доступний іншим за своїм іменем. У нашому прикладі PHP-код підключається до бази не за localhost і не за IP, а за іменем сервісу — db:

$pdo = new PDO('mysql:host=db;dbname=mysite', 'mysite', $password);
//                     ^^ ім'я сервісу з docker-compose.yml

Саме тому порт бази не треба публікувати назовні (ports:) — контейнери спілкуються всередині приватної мережі Docker. Публікувати порт бази варто лише тоді, коли до неї треба підключитись із хоста (наприклад, з DBeaver для дебагу) — і тоді прив'язуйте його до 127.0.0.1:

  db:
    ports:
      - "127.0.0.1:3306:3306"   # доступ лише з localhost, не з інтернету

За потреби ізолювати групи сервісів (наприклад, щоб фронтенд не мав доступу до бази напряму) створюють кілька мереж явно:

services:
  nginx:
    networks: [frontend]
  php:
    networks: [frontend, backend]
  db:
    networks: [backend]        # db недосяжна з nginx

networks:
  frontend:
  backend:

Змінні середовища: три способи

Передати конфігурацію в контейнер можна кількома шляхами, від найпростішого до найчистішого:

  php:
    # 1. Напряму в compose
    environment:
      APP_ENV: production
      APP_DEBUG: "false"

    # 2. З окремого файлу (зручно для довгих списків)
    env_file:
      - ./app.env

    # 3. Підстановка зі змінних хоста / .env поруч із compose
    environment:
      DB_PASSWORD: ${DB_PASSWORD}

Файл .env (поруч із docker-compose.yml) Compose читає автоматично й підставляє значення в ${...}. Не плутайте його з env_file: — перший впливає на сам compose-файл, другий передає змінні всередину контейнера.

Кілька файлів: dev і prod без дублювання

Один із найкорисніших прийомів. Базовий docker-compose.yml описує спільне, а docker-compose.override.yml — те, що потрібно лише для розробки (bind mount вихідників, відкриті порти, xdebug). Compose автоматично зливає обидва файли:

# docker-compose.override.yml — підхоплюється сам при `up`
services:
  php:
    volumes:
      - ./src:/var/www/html    # монтуємо код для гарячої правки
    environment:
      APP_DEBUG: "true"
  db:
    ports:
      - "127.0.0.1:3306:3306"  # відкриваємо базу для локального дебагу

А для продакшну тримаємо окремий файл і зливаємо явно, щоб override НЕ підхопився:

# Локально — base + override автоматично
docker compose up -d

# На проді — base + prod, без dev-override
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

DRY через YAML-якорі

Коли кілька сервісів мають однакові налаштування (логи, restart-політика, лейбли), їх можна винести в якір і перевикористати через <<: *anchor:

x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options: { max-size: "10m", max-file: "3" }

services:
  php:
    <<: *common
    build: .
  nginx:
    <<: *common
    image: nginx:alpine

Секція x-common з префіксом x- ігнорується Docker'ом як «розширення» — вона існує лише щоб на неї посилатись. Тепер restart-політику й логування описано в одному місці для всіх сервісів.

Profiles: вмикати сервіси за потреби

Іноді частина сервісів потрібна не завжди — наприклад, adminer для роботи з базою чи воркер черг. profiles дозволяє тримати їх в одному файлі, але піднімати вибірково:

  adminer:
    image: adminer
    ports:
      - "127.0.0.1:8081:8080"
    profiles: [tools]
docker compose up -d                    # adminer НЕ підніметься
docker compose --profile tools up -d    # підніметься і adminer

Корисні команди Compose

docker compose config          # показати підсумковий склад (base+override) — дебаг помилок
docker compose ps              # статус сервісів стеку
docker compose restart php     # перезапустити один сервіс
docker compose up -d --build   # перезібрати образи й підняти
docker compose exec php sh     # зайти в контейнер сервісу
docker compose down -v         # знести стек РАЗОМ із томами (обережно!)

Особливо цінна перша — docker compose config: вона показує, у що саме перетворюється ваш стек після злиття всіх файлів і підстановки змінних. Перше, що варто запустити, коли «чомусь не працює».

Крок 4: власний образ через Dockerfile

Готові образи хороші, але рано чи пізно знадобиться свій — із потрібними PHP-розширеннями, скопійованим кодом, налаштуваннями. Це описується у Dockerfile:

FROM php:8.3-fpm-alpine

# Системні залежності та PHP-розширення
RUN apk add --no-cache libpng-dev libzip-dev \
    && docker-php-ext-install pdo_mysql gd zip opcache

# Робочий каталог
WORKDIR /var/www/html

# Спочатку — тільки залежності (кешування шарів!)
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts

# Потім — увесь код
COPY . .

EXPOSE 9000
CMD ["php-fpm"]

Ключовий трюк тут — порядок шарів. Docker кешує кожну інструкцію. Якщо спершу скопіювати composer.json і поставити залежності, а код — окремим шаром нижче, то при зміні коду (але не залежностей) Docker перевикористає кеш і не перевстановлюватиме composer щоразу. Це економить хвилини на кожній збірці.

Збірка й використання:

docker build -t mysite:latest .

А в compose замість image: вказуємо build:

  php:
    build: .
    volumes:
      - ./src:/var/www/html

Крок 5: багатоетапна збірка (multi-stage)

Для фронтенду чи компільованих мов образ роздувається інструментами збірки, які в продакшні не потрібні. Multi-stage build дозволяє зібрати в «жирному» образі, а в фінальний перекласти лише результат:

# Етап збірки
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Фінальний образ — тільки зібрана статика
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

Результат — фінальний образ важить десятки мегабайт замість сотень: у ньому немає ані Node.js, ані node_modules, тільки готові файли й nginx.

Крок 6: що змінюється для продакшну

Стек, який працює на ноутбуку, і той самий стек у бою — це різні речі. Ось що обов'язково додати.

Політика перезапуску

Щоб контейнери самі піднімались після падіння чи перезавантаження сервера:

services:
  php:
    restart: unless-stopped

unless-stopped — оптимальний вибір: перезапускає завжди, крім випадку, коли ви зупинили контейнер вручну.

Healthcheck

Docker має знати, що контейнер не просто «запущений», а реально працює:

  db:
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect"]
      interval: 10s
      timeout: 5s
      retries: 5

У парі з depends_on: condition: service_healthy це гарантує, що застосунок стартує лише після того, як база реально готова приймати з'єднання, а не просто «контейнер запустився».

Секрети — не в git

Паролі й ключі виносимо у файл .env, який обов'язково додаємо в .gitignore. У репозиторій кладемо тільки .env.example із порожніми значеннями. Compose підхоплює .env автоматично:

# .env (не в git!)
DB_ROOT_PASSWORD=надійний_пароль
DB_PASSWORD=інший_пароль

Ліміти ресурсів

Щоб один контейнер, який «поїхав», не з'їв усю пам'ять сервера:

  php:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

Керування логами

За замовчуванням логи контейнерів ростуть безмежно й здатні забити диск. Обмежуємо ротацію:

  php:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Не запускати від root

Усередині контейнера процес за замовчуванням працює від root. Для продакшну краще створити непривілейованого користувача в Dockerfile і вказати USER appuser — якщо контейнер зламають, зловмисник не отримає root-прав навіть усередині.

Крок 7: reverse proxy й HTTPS

У продакшні перед контейнерами зазвичай стоїть reverse proxy, який завершує TLS і маршрутизує домени. Два популярні підходи:

  • Nginx на хості + certbot — класика, повний контроль, порти контейнерів прив'язуємо тільки до 127.0.0.1, щоб вони не стирчали назовні.
  • Traefik у контейнері — автоматично підхоплює нові сервіси через Docker-мітки й сам отримує Let's Encrypt-сертифікати. Зручно, коли сервісів багато.

Якщо сервер за NAT без білого IP (типово для університетської чи домашньої інфраструктури) — Cloudflare Tunnel прокидає трафік назовні без відкриття портів узагалі, достатньо додати контейнер cloudflared у той самий compose.

Крок 8: оновлення та бекапи

Оновлення образів:

docker compose pull        # завантажити нові версії образів
docker compose up -d       # перестворити контейнери з оновленнями
docker image prune -f      # прибрати старі непотрібні образи

Бекап named volume (наприклад, бази) — запускаємо тимчасовий контейнер, який пакує том у архів:

docker run --rm \
  -v mysite_dbdata:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/dbdata-$(date +%F).tar.gz -C /data .

Для бази даних надійніше робити логічний дамп, а не копіювати файли «наживо»:

docker compose exec -T db \
  mysqldump -u root -p"$DB_ROOT_PASSWORD" mysite > backup.sql

Гігієна: не засмічуйте систему

Зупинені контейнери, старі образи й «завислі» томи з часом з'їдають десятки гігабайтів. Періодичне прибирання:

docker system df      # скільки місця займає Docker
docker system prune   # прибрати зупинене й непотрібне
docker system prune -a --volumes   # агресивне (обережно з --volumes!)

--volumes видаляє невикористовувані томи — переконайтеся, що серед них немає потрібних даних.

Типові помилки новачків

  • Дані в контейнері замість тома. Забули винести дані бази в volume → docker compose down → усе зникло. Усе, що має пережити перезапуск, — тільки в томах.
  • latest у продакшні. Тег latest непередбачуваний: сьогодні одна версія, завтра інша. У бою фіксуйте конкретні теги (mariadb:11.4, а не mariadb:latest).
  • Паролі в docker-compose.yml. Файл потрапляє в git → секрети злиті. Тільки через .env.
  • Порти назовні без потреби. ports: "3306:3306" для бази відкриває її всьому інтернету. Якщо база потрібна лише іншим контейнерам — порт узагалі не публікуємо, вони спілкуються через внутрішню мережу Docker за іменем сервісу.
  • Ігнорування .dockerignore. Без нього в образ потрапляють node_modules, .git і локальні файли, роздуваючи його й уповільнюючи збірку.

Висновок

Шлях від першого docker run до продакшну — це поступове нарощування: спочатку запуск готового образу, потім опис стеку в Compose, власний Dockerfile із кешуванням шарів, а для бою — політики перезапуску, healthcheck'и, ліміти ресурсів, винесені секрети, reverse proxy й налаштовані бекапи. Кожен крок додає надійності, і жоден не вимагає глибокого занурення в нутрощі Linux.

Головна цінність Docker для веброзробника — відтворюваність: той самий docker compose up -d підніме ідентичне середовище на будь-якому сервері. А коли весь стек описано в кількох текстових файлах, його легко версіонувати, ревʼювити й переносити. У наступних матеріалах розберемо конкретні продакшн-кейси: розгортання складних застосунків і публікацію через Cloudflare Tunnel без білого IP.