Пошук уроків, статей та іншого контенту
Зберете багатоконтейнерний застосунок із Node.js, PostgreSQL та Redis, налаштувавши мережі, volumes і healthchecks.
У цій практиці ви зберете багатоконтейнерний застосунок із трьох сервісів:
Node.js — HTTP-застосунок;
PostgreSQL — реляційна база даних;
Redis — сховище лічильника в пам’яті.
У процесі ви налаштуєте:
взаємодію контейнерів через імена сервісів;
окремі Docker-мережі;
іменовані volumes для збереження даних;
healthcheck для перевірки готовності сервісів;
порядок запуску через depends_on;
конфігурацію через змінні середовища.
Створіть каталог проєкту з такою структурою:
node-postgres-redis/
├── src/
│ └── server.js
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
└── package.jsonpackage.json{
"name": "node-postgres-redis",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/server.js"
},
"dependencies": {
"pg": "^8.13.1",
"redis": "^4.7.0"
}
}Пакет pg використовується для підключення до PostgreSQL, а пакет redis — для роботи з Redis.
src/server.jsimport http from 'node:http';
import pg from 'pg';
import { createClient } from 'redis';
const { Pool } = pg;
const port = Number(process.env.PORT || 3000);
const databaseUrl =
process.env.DATABASE_URL ||
'postgres://app_user:app_password@postgres:5432/app_db';
const redisUrl =
process.env.REDIS_URL ||
'redis://redis:6379';
const pool = new Pool({
connectionString: databaseUrl
});
const redis = createClient({
url: redisUrl
});
redis.on('error', (error) => {
console.error('Помилка Redis:', error.message);
});
async function initializeDatabase() {
await pool.query(`
CREATE TABLE IF NOT EXISTS requests (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
}
async function initializeRedis() {
if (!redis.isOpen) {
await redis.connect();
}
}
async function handleRequest(request, response) {
try {
if (request.url === '/health') {
await pool.query('SELECT 1');
await redis.ping();
response.writeHead(200, {
'Content-Type': 'application/json'
});
response.end(JSON.stringify({
status: 'ok',
postgres: 'ok',
redis: 'ok'
}));
return;
}
if (request.url === '/counter') {
await pool.query('INSERT INTO requests DEFAULT VALUES');
const counter = await redis.incr('requests:counter');
response.writeHead(200, {
'Content-Type': 'application/json'
});
response.end(JSON.stringify({
postgresRequests: counter,
message: 'Запит збережено'
}));
return;
}
response.writeHead(200, {
'Content-Type': 'application/json'
});
response.end(JSON.stringify({
message: 'Node.js працює',
endpoints: ['/health', '/counter']
}));
} catch (error) {
console.error('Помилка обробки запиту:', error);
response.writeHead(503, {
'Content-Type': 'application/json'
});
response.end(JSON.stringify({
status: 'error',
message: 'Залежність недоступна'
}));
}
}
async function start() {
await initializeRedis();
await initializeDatabase();
const server = http.createServer(handleRequest);
server.listen(port, '0.0.0.0', () => {
console.log(`HTTP-сервер слухає порт ${port}`);
});
}
async function shutdown(signal) {
console.log(`Отримано ${signal}. Завершення роботи...`);
await redis.quit();
await pool.end();
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
start().catch((error) => {
console.error('Не вдалося запустити застосунок:', error);
process.exit(1);
});Застосунок має два основні маршрути:
GET /health — перевіряє доступність PostgreSQL і Redis;
GET /counter — додає рядок у PostgreSQL та збільшує лічильник у Redis.
Зверніть увагу на адреси підключення:
postgres://app_user:app_password@postgres:5432/app_db
redis://redis:6379postgres і redis — це не localhost, а імена сервісів у Docker Compose. Docker DNS автоматично дозволяє контейнерам знаходити один одного за цими іменами.
Створіть файл Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY src ./src
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
USER node
CMD ["npm", "start"]У цьому Dockerfile:
використовується легкий образ node:22-alpine;
робочим каталогом стає /app;
спочатку копіюється package.json, щоб Docker міг кешувати шар встановлення залежностей;
застосунок запускається від непривілейованого користувача node;
Node.js слухає порт 3000.
Файл .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose.ymlСтворіть файл docker-compose.yml:
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
PORT: 3000
DATABASE_URL: postgres://app_user:app_password@postgres:5432/app_db
REDIS_URL: redis://redis:6379
ports:
- "3000:3000"
networks:
- app_network
- data_network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:3000/health').then(response => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- data_network
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U app_user -d app_db"
]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- data_network
healthcheck:
test:
[
"CMD",
"redis-cli",
"ping"
]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
volumes:
postgres_data:
redis_data:
networks:
app_network:
driver: bridge
data_network:
driver: bridge
internal: trueappСервіс app збирається з локального Dockerfile:
build:
context: .
dockerfile: DockerfileПорт контейнера 3000 публікується на порт 3000 хоста:
ports:
- "3000:3000"Це означає, що застосунок буде доступний на хості за адресою:
http://localhost:3000Застосунок підключений до двох мереж:
networks:
- app_network
- data_networkapp має доступ до data_network, тому може підключатися до PostgreSQL і Redis. Водночас PostgreSQL і Redis не підключені до app_network.
postgresЗмінні середовища PostgreSQL визначають початкову базу даних і користувача:
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_passwordPostgreSQL зберігає дані у volume:
volumes:
- postgres_data:/var/lib/postgresql/dataІменований volume не залежить від життєвого циклу контейнера. Якщо контейнер PostgreSQL видалити й створити знову, дані залишаться у postgres_data.
redisRedis запускається з увімкненим AOF-журналом:
command: redis-server --appendonly yesAOF допомагає зберігати операції Redis у файловій системі. Дані Redis монтуються у volume:
volumes:
- redis_data:/dataУ конфігурації є дві мережі:
networks:
app_network:
driver: bridge
data_network:
driver: bridge
internal: truedata_network призначена для внутрішньої взаємодії між застосунком і сховищами.
Параметр internal: true забороняє зовнішній доступ до цієї мережі. PostgreSQL і Redis не мають опублікованих портів на хост, тому підключитися до них напряму через localhost:5432 або localhost:6379 неможливо.
Це зменшує поверхню атаки: назовні відкритий лише HTTP-порт Node.js.
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U app_user -d app_db"
]Команда pg_isready перевіряє, чи PostgreSQL приймає підключення до бази app_db.
Це важливіше, ніж просто перевірка факту запуску контейнера. Контейнер може бути запущений, але сам PostgreSQL ще може ініціалізуватися.
healthcheck:
test:
[
"CMD",
"redis-cli",
"ping"
]Redis вважається здоровим, якщо повертає відповідь на команду PING.
Для Node.js використовується вбудований fetch:
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:3000/health').then(response => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
]Перевірка звертається до маршруту /health, який додатково виконує:
SELECT 1 у PostgreSQL;
PING у Redis.
Тому статус healthy для app означає, що HTTP-сервер працює і дві його залежності доступні.
У app вказано:
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthyCompose спочатку запустить PostgreSQL і Redis, дочекається їхніх успішних healthchecks, а вже потім запустить app.
Важливо: depends_on керує порядком запуску, але не замінює обробку помилок у застосунку. Сам Node.js-застосунок все одно має коректно завершуватися або повторювати підключення, якщо залежність стане недоступною після запуску.
У корені проєкту виконайте:
docker compose up --buildПараметр --build змушує Compose зібрати образ Node.js з актуальним Dockerfile.
Для запуску у фоновому режимі:
docker compose up --build -dПеревірте стан контейнерів:
docker compose psОчікуваний стан сервісів:
NAME SERVICE STATUS
app app running (healthy)
postgres postgres running (healthy)
redis redis running (healthy)Назви контейнерів можуть відрізнятися, оскільки Compose додає до них ім’я проєкту.
Перевірте healthcheck HTTP-застосунку:
curl http://localhost:3000/healthОчікувана відповідь:
{"status":"ok","postgres":"ok","redis":"ok"}Збільште лічильник:
curl http://localhost:3000/counterПриклад відповіді:
{"postgresRequests":1,"message":"Запит збережено"}Повторіть команду:
curl http://localhost:3000/counterЛічильник у Redis збільшиться:
{"postgresRequests":2,"message":"Запит збережено"}При цьому кожен виклик також додає рядок до таблиці requests у PostgreSQL.
Перегляньте журнали застосунку:
docker compose logs -f appПереглянути журнали конкретного сервісу можна так:
docker compose logs postgres
docker compose logs redisОтримайте список мереж:
docker network lsЩоб переглянути підключені контейнери та параметри мережі:
docker network inspect node-postgres-redis_data_networkФактичний префікс може відрізнятися, якщо каталог проєкту має іншу назву або задано інше ім’я Compose-проєкту.
Усередині мережі застосунок звертається до сервісів за такими DNS-іменами:
postgres
redisПорти баз даних не потрібно публікувати на хост. Вони доступні контейнеру app безпосередньо через внутрішню мережу:
postgres:5432
redis:6379Перегляньте список volume:
docker volume lsСеред них мають бути volumes для PostgreSQL і Redis.
Зупиніть і видаліть контейнери:
docker compose downПісля цього запустіть проєкт знову:
docker compose up -dДані PostgreSQL і Redis залишаться, оскільки команда down за замовчуванням не видаляє іменовані volumes.
Перевірте це повторним запитом:
curl http://localhost:3000/counterЛічильник Redis продовжить значення, яке мав до перезапуску.
Щоб видалити також volumes і почати з чистого стану:
docker compose down -vКоманда
docker compose down -vвидаляє дані PostgreSQL і Redis. Використовуйте її лише тоді, коли ці дані більше не потрібні.
Типовий цикл роботи з проєктом:
docker compose up --build -d
docker compose ps
docker compose logs -f app
docker compose downЯкщо змінено лише код Node.js, для цього Dockerfile потрібна повторна збірка:
docker compose up --build -d appЯкщо змінено package.json, також потрібно перебудувати образ, щоб нові залежності встановилися всередині контейнера.
localhost для підключення до PostgreSQL або RedisУ контейнері localhost означає сам контейнер Node.js, а не контейнер PostgreSQL чи Redis.
Неправильно:
postgres://app_user:app_password@localhost:5432/app_dbПравильно:
postgres://app_user:app_password@postgres:5432/app_dbАналогічно для Redis:
redis://redis:6379Для внутрішньої взаємодії не потрібні такі налаштування:
ports:
- "5432:5432"або:
ports:
- "6379:6379"Застосунок знаходить сервіси через Docker-мережу. Публікувати ці порти на хост потрібно лише за окремої потреби, наприклад для локального адміністративного підключення.
Статус running не означає, що PostgreSQL або Redis уже готові приймати запити.
Саме тому використовуються healthchecks і умови:
condition: service_healthyКоманда:
docker compose down -vвидаляє дані сховищ. Для звичайного перезапуску використовуйте:
docker compose down0.0.0.0 у Node.jsЗастосунок у контейнері має слухати всі мережеві інтерфейси:
server.listen(port, '0.0.0.0');Якщо слухати лише 127.0.0.1, порт може бути недоступним з інших контейнерів або з хоста.
Для офіційного образу PostgreSQL дані потрібно монтувати у:
/var/lib/postgresql/dataМонтування в інший каталог не забезпечить збереження фактичного каталогу даних PostgreSQL.
У цій практиці ви створили багатоконтейнерний застосунок, у якому:
Node.js працює в окремому контейнері;
PostgreSQL і Redis доступні через імена сервісів;
app підключений до мережі застосунку та внутрішньої мережі даних;
PostgreSQL і Redis не публікують порти назовні;
дані зберігаються в іменованих volumes;
healthchecks перевіряють реальну готовність сервісів;
depends_on запускає Node.js після готовності PostgreSQL і Redis;
/health перевіряє роботу всього ланцюжка залежностей.