м. Київ, вул. Кирилівська 102

28 вересня 2026 р.

Побудова Dev середовища через Docker Compose

Побудова Dev середовища через Docker Compose

Локальне середовище часто починається з інструкції на кілька сторінок: установити потрібну версію Node.js, підняти PostgreSQL, створити користувача, налаштувати Redis, скопіювати змінні та не зайняти чужий порт. Через місяць частина команд уже не відповідає репозиторію, а результат залежить від операційної системи розробника. Docker Compose дає іншу модель: склад середовища, мережі, volumes, перевірки готовності та команди запуску зберігаються поруч із кодом.

У цій інструкції побудуємо dev-контур для API на Node.js із PostgreSQL, Redis і опціональним Adminer. Застосунок працюватиме у контейнері з hot reload, база зберігатиме дані між перезапусками, а `healthcheck` не дозволить API стартувати раніше за залежності. Для системного впровадження контейнерних практик можна залучити DevOps-послуги ITheal, а автоматичні перевірки Compose-конфігурації варто додати до налаштування CI/CD.

Структура dev-середовища та підготовка застосунку

Приклад розрахований на Docker Engine із сучасним plugin `docker compose`. Для режиму Compose Watch потрібна версія Docker Compose 2.22.0 або новіша. Перевірте інструменти до створення контейнерів:

bash
docker version
docker compose version

Структура репозиторію буде такою:

text
compose-dev-demo/
├── src/
│   └── server.js
├── secrets/
│   └── postgres_password.txt
├── .dockerignore
├── .env.example
├── compose.yaml
├── Dockerfile
├── package.json
└── package-lock.json

Створіть мінімальний застосунок:

bash
mkdir -p compose-dev-demo/src compose-dev-demo/secrets
cd compose-dev-demo
npm init -y
npm install express pg redis
npm install --save-dev nodemon

У `package.json` додайте команди запуску:

json
{
  "scripts": {
    "dev": "nodemon --legacy-watch src/server.js",
    "start": "node src/server.js"
  }
}

Опція `--legacy-watch` використовує polling. Вона споживає більше ресурсів, але надійніше бачить зміни файлів у Docker Desktop, WSL і деяких мережевих файлових системах.

Файл `src/server.js` перевірятиме обидві залежності й повертатиме простий health endpoint:

javascript
const express = require("express");
const { Pool } = require("pg");
const { createClient } = require("redis");

const app = express();
const port = Number(process.env.PORT || 3000);

const pool = new Pool({
  host: process.env.POSTGRES_HOST,
  port: Number(process.env.POSTGRES_PORT || 5432),
  database: process.env.POSTGRES_DB,
  user: process.env.POSTGRES_USER,
  password: process.env.POSTGRES_PASSWORD,
});

const redis = createClient({ url: process.env.REDIS_URL });

app.get("/health", async (_request, response) => {
  await pool.query("SELECT 1");
  await redis.ping();
  response.json({ status: "ok" });
});

async function start() {
  await redis.connect();
  app.listen(port, "0.0.0.0", () => {
    console.log(`API listens on port ${port}`);
  });
}

start().catch((error) => {
  console.error(error);
  process.exit(1);
});

Усередині Compose-мережі сервіси звертаються один до одного за DNS-іменами `db` і `redis`. Значення `localhost` у контейнері означає сам контейнер, а не хост і не сусідню базу.

Dockerfile, змінні та секрети без зайвого сміття

Dev-образ має відтворювати версію runtime і залежності, але не повинен запускати процес від root. Створіть `Dockerfile`:

dockerfile
FROM node:22-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY --chown=node:node src ./src

USER node

EXPOSE 3000

CMD ["npm", "run", "dev"]

Окреме копіювання package-файлів дозволяє Docker повторно використовувати layer із `npm ci`, доки залежності не змінилися. Для build context додайте `.dockerignore`:

text
node_modules
npm-debug.log
.git
.env
secrets
coverage
dist

`.env` у Compose зручно використовувати для несекретних параметрів та interpolation. Репозиторій має містити лише шаблон `.env.example`:

dotenv
COMPOSE_PROJECT_NAME=compose-dev-demo
API_PORT=3000
POSTGRES_DB=app_dev
POSTGRES_USER=app_dev

Робочу копію створіть локально:

bash
cp .env.example .env
printf '%s' 'change-this-local-password' > secrets/postgres_password.txt
chmod 600 secrets/postgres_password.txt

Додайте `.env` і каталог `secrets/` до `.gitignore`. Пароль не слід записувати у `compose.yaml`, Dockerfile або commit history. Compose secret монтується як файл у `/run/secrets/...`; застосунок може прочитати його через entrypoint або підтримувану змінну з суфіксом `_FILE`. Офіційний образ PostgreSQL підтримує `POSTGRES_PASSWORD_FILE`, тому пароль не потрібно дублювати у звичайному environment.

Для нашого Node.js прикладу найпростіше передати пароль через коротку shell-команду запуску, яка читає файл без виведення значення в log:

yaml
command:
  - /bin/sh
  - -c
  - export POSTGRES_PASSWORD="$$(cat /run/secrets/postgres_password)" && npm run dev

Подвійний знак `$` важливий: він залишає підстановку контейнерному shell. Одинарний `${...}` Compose спробував би інтерполювати ще на хості.

Повний compose.yaml із healthcheck, profiles і Watch

Створіть `compose.yaml` без застарілого поля `version`. Сучасний Compose використовує актуальну Compose Specification:

yaml
name: ${COMPOSE_PROJECT_NAME:-compose-dev-demo}

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    command:
      - /bin/sh
      - -c
      - export POSTGRES_PASSWORD="$$(cat /run/secrets/postgres_password)" && npm run dev
    environment:
      NODE_ENV: development
      PORT: "3000"
      POSTGRES_HOST: db
      POSTGRES_PORT: "5432"
      POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB is required}
      POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required}
      REDIS_URL: redis://redis:6379
    ports:
      - "127.0.0.1:${API_PORT:-3000}:3000"
    secrets:
      - postgres_password
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test:
        - CMD
        - node
        - -e
        - fetch('http://127.0.0.1:3000/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 20s
    init: true
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          initial_sync: true
        - action: rebuild
          path: ./package.json
        - action: rebuild
          path: ./package-lock.json

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB is required}
      POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required}
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  adminer:
    image: adminer:5
    profiles: ["tools"]
    environment:
      ADMINER_DEFAULT_SERVER: db
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      db:
        condition: service_healthy

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt

volumes:
  postgres_data:
  redis_data:

Прив’язка портів до `127.0.0.1` не відкриває dev-сервіси всій локальній мережі. PostgreSQL і Redis узагалі не публікують порти: API бачить їх через внутрішню default network. Якщо потрібно під’єднати desktop SQL client, створіть локальний override з тимчасовим mapping, а не відкривайте базу в основному файлі для всіх учасників.

`depends_on` визначає порядок створення, але сам по собі не гарантує готовність процесу. Умова `service_healthy` змушує Compose чекати успішного healthcheck. Це прибирає крихкі конструкції на кшталт `sleep 10`, які іноді надто довгі, а іноді недостатні.

Сервіс Adminer має profile `tools`, тому звичайний запуск не витрачає на нього ресурси. Core-сервіси залишаються без profiles і стартують завжди. Compose Watch синхронізує лише `src`, а зміни package-файлів перебудовують image. Так `node_modules` лишається всередині Linux-контейнера й не конфліктує з native modules хостової платформи.

Запуск, перевірка та щоденна робота

Перед першим запуском перевірте підсумкову модель. Команда покаже результат interpolation і знайде синтаксичні помилки без створення контейнерів:

bash
docker compose config --quiet
docker compose config --services

Не публікуйте повний результат `docker compose config` у tickets або чатах: після interpolation він може містити чутливі environment values. Запустіть dev-контур у режимі Watch:

bash
docker compose up --build --watch

В іншому терміналі перевірте стан:

bash
docker compose ps
docker compose logs --tail=100 api db redis
curl --fail http://127.0.0.1:3000/health

У відповіді має бути `{"status":"ok"}`, а всі три core-сервіси повинні перейти у стан healthy. Змініть `src/server.js` і повторіть curl: Compose синхронізує файл, після чого nodemon перезапустить Node.js процес.

Adminer запускається лише за потреби:

bash
docker compose --profile tools up -d adminer
docker compose --profile tools ps

Відкрийте `http://127.0.0.1:8080`, у полі Server укажіть `db`, а не `localhost`. Облікові дані беруться з локального `.env` і secret-файлу.

Для одноразових команд використовуйте `run --rm`, щоб не залишати зупинені контейнери:

bash
docker compose run --rm api npm test
docker compose exec db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"

Друга команда використовує змінні host shell. Якщо вони не експортовані, передайте значення явно або виконайте shell усередині контейнера:

bash
docker compose exec db sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

Звичайна зупинка зберігає named volumes:

bash
docker compose down

Повне очищення локальних даних є руйнівною операцією:

bash
docker compose down --volumes --remove-orphans

Перед `--volumes` переконайтеся, що локальна база не містить потрібних тестових даних. Для командної роботи корисно мати окремі scripts `dev-up`, `dev-down`, `dev-reset` і короткий README, але вони повинні лише обгортати перевірені Compose-команди, а не приховувати складну логіку.

Якщо Docker Engine ще не налаштований, спочатку пройдіть інструкцію Встановлення Docker та Docker Compose на Ubuntu, а потім повертайтеся до репозиторію середовища.

Типові помилки

- Використовувати `localhost` для доступу до PostgreSQL або Redis із API-контейнера. У Compose потрібно звертатися за service name: `db` або `redis`. - Вважати `depends_on` перевіркою готовності. Без `healthcheck` Compose знає лише, що процес контейнера запущений. - Монтувати `.:/app` разом із host `node_modules`. Це створює проблеми з native dependencies, правами й продуктивністю на Docker Desktop. Синхронізуйте лише source-каталоги або використовуйте окремий volume. - Додавати `.env`, дампи та файли `secrets/` до Git. Навіть локальний пароль після commit може залишитися в історії. - Публікувати порти бази на `0.0.0.0`. Для dev-інструментів достатньо `127.0.0.1`, а core-залежності часто не потребують host mapping взагалі. - Використовувати mutable image tags без плану оновлення. Для стабільності команди фіксуйте узгоджені major/minor versions, перевіряйте release notes і оновлюйте залежності окремим merge request. - Запускати всі допоміжні інструменти постійно. Profiles дозволяють піднімати Adminer, mail catcher або debug proxy лише тоді, коли вони потрібні. - Виконувати `docker compose down -v` як звичайну команду зупинки. Прапорець видаляє named volumes разом із локальними даними. - Переносити dev-файл у production без перегляду. Hot reload, source sync, Adminer, відкриті debug endpoints і локальні secrets не належать production-контуру.

Висновок

Якісне dev-середовище через Docker Compose — це не просто список контейнерів. Воно фіксує версії runtime і залежностей, використовує service discovery замість випадкових host-адрес, чекає реальної готовності через healthcheck, зберігає дані у named volumes і не відкриває зайві порти. Compose Watch скорочує цикл між редагуванням коду та перевіркою, а profiles не перевантажують базовий стек допоміжними сервісами.

Зберігайте `compose.yaml`, Dockerfile, `.dockerignore` і `.env.example` у репозиторії та перевіряйте їх у CI. Секрети залишайте поза Git, повне очищення volumes робіть лише свідомо, а production-конфігурацію проєктуйте окремо. Тоді новий учасник команди отримує передбачуване середовище однією командою, а відмінності між робочими станціями перестають бути прихованою частиною архітектури.

📞