28 вересня 2026 р.
Побудова 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 або новіша. Перевірте інструменти до створення контейнерів:
docker version
docker compose versionСтруктура репозиторію буде такою:
compose-dev-demo/
├── src/
│ └── server.js
├── secrets/
│ └── postgres_password.txt
├── .dockerignore
├── .env.example
├── compose.yaml
├── Dockerfile
├── package.json
└── package-lock.jsonСтворіть мінімальний застосунок:
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` додайте команди запуску:
{
"scripts": {
"dev": "nodemon --legacy-watch src/server.js",
"start": "node src/server.js"
}
}Опція `--legacy-watch` використовує polling. Вона споживає більше ресурсів, але надійніше бачить зміни файлів у Docker Desktop, WSL і деяких мережевих файлових системах.
Файл `src/server.js` перевірятиме обидві залежності й повертатиме простий health endpoint:
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`:
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`:
node_modules
npm-debug.log
.git
.env
secrets
coverage
dist`.env` у Compose зручно використовувати для несекретних параметрів та interpolation. Репозиторій має містити лише шаблон `.env.example`:
COMPOSE_PROJECT_NAME=compose-dev-demo
API_PORT=3000
POSTGRES_DB=app_dev
POSTGRES_USER=app_devРобочу копію створіть локально:
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:
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:
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 і знайде синтаксичні помилки без створення контейнерів:
docker compose config --quiet
docker compose config --servicesНе публікуйте повний результат `docker compose config` у tickets або чатах: після interpolation він може містити чутливі environment values. Запустіть dev-контур у режимі Watch:
docker compose up --build --watchВ іншому терміналі перевірте стан:
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 запускається лише за потреби:
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`, щоб не залишати зупинені контейнери:
docker compose run --rm api npm test
docker compose exec db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"Друга команда використовує змінні host shell. Якщо вони не експортовані, передайте значення явно або виконайте shell усередині контейнера:
docker compose exec db sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'Звичайна зупинка зберігає named volumes:
docker compose downПовне очищення локальних даних є руйнівною операцією:
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-конфігурацію проєктуйте окремо. Тоді новий учасник команди отримує передбачуване середовище однією командою, а відмінності між робочими станціями перестають бути прихованою частиною архітектури.