Пошук уроків, статей та іншого контенту
Зберете повний стек у Docker Compose: Next.js, NestJS, PostgreSQL і Redis із мережами, томами та режимами запуску.
У цій практиці ми зберемо застосунок із чотирьох сервісів:
Next.js — клієнтський вебзастосунок;
NestJS — HTTP API;
PostgreSQL — реляційна база даних;
Redis — швидке сховище даних і кеш.
Docker Compose налаштує:
окремі контейнери для кожного сервісу;
внутрішні мережі;
іменовані томи для збереження даних;
перевірки готовності PostgreSQL і Redis;
режими розробки та production;
передавання конфігурації через змінні середовища.
Створимо таку структуру:
fullstack-app/
├── compose.yaml
├── compose.dev.yaml
├── compose.prod.yaml
├── api/
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── package.json
│ └── src/
│ ├── app.module.ts
│ ├── main.ts
│ └── status/
│ ├── status.controller.ts
│ └── status.service.ts
└── web/
├── Dockerfile
├── Dockerfile.prod
├── package.json
└── app/
└── page.jsПроєкти можна створити за допомогою стандартних генераторів:
nest new api
npx create-next-app@latest webДля Next.js оберіть App Router. У прикладах нижче використовується JavaScript для вебзастосунку та TypeScript для API.
Встановіть клієнти PostgreSQL і Redis у каталозі api:
cd api
npm install pg ioredisУ api/src/main.ts запустимо HTTP-сервер на порту 3001:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT || 3001, '0.0.0.0');
}
bootstrap();Адреса 0.0.0.0 важлива для Docker. Якщо застосунок слухає лише localhost, інші контейнери або хостова система не зможуть підключитися до нього.
Файл api/src/app.module.ts:
import { Module } from '@nestjs/common';
import { StatusController } from './status/status.controller';
import { StatusService } from './status/status.service';
@Module({
controllers: [StatusController],
providers: [StatusService],
})
export class AppModule {}Файл api/src/status/status.controller.ts:
import { Controller, Get } from '@nestjs/common';
import { StatusService } from './status.service';
@Controller('status')
export class StatusController {
constructor(private readonly statusService: StatusService) {}
@Get()
getStatus() {
return this.statusService.getStatus();
}
}Файл api/src/status/status.service.ts:
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { Pool } from 'pg';
import Redis from 'ioredis';
@Injectable()
export class StatusService implements OnModuleDestroy {
private readonly database = new Pool({
connectionString: process.env.DATABASE_URL,
});
private readonly redis = new Redis(
process.env.REDIS_URL || 'redis://localhost:6379',
);
async getStatus() {
const databaseResult = await this.database.query('SELECT NOW() AS now');
await this.redis.set('last_status_check', new Date().toISOString());
const lastStatusCheck = await this.redis.get('last_status_check');
return {
api: 'ok',
postgres: {
connected: true,
serverTime: databaseResult.rows[0].now,
},
redis: {
connected: true,
lastStatusCheck,
},
};
}
async onModuleDestroy() {
await this.database.end();
await this.redis.quit();
}
}У контейнерному середовищі значення DATABASE_URL має використовувати ім’я сервісу postgres, а не localhost:
postgresql://app:app_password@postgres:5432/appТак само Redis буде доступний за адресою:
redis://redis:6379У Docker Compose ім’я сервісу автоматично працює як DNS-ім’я всередині спільної мережі.
У стандартному api/package.json мають бути доступні такі скрипти:
{
"scripts": {
"build": "nest build",
"start": "node dist/main",
"start:dev": "nest start --watch"
}
}Після встановлення залежностей у каталозі api повинен з’явитися package-lock.json. Він потрібен для відтворюваного встановлення залежностей через npm ci.
Файл web/app/page.js:
'use client';
import { useEffect, useState } from 'react';
export default function HomePage() {
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`${process.env.NEXT_PUBLIC_API_URL}/status`)
.then((response) => {
if (!response.ok) {
throw new Error('API повернув помилку');
}
return response.json();
})
.then(setStatus)
.catch((requestError) => {
setError(requestError.message);
});
}, []);
return (
<main style={{ padding: 32, fontFamily: 'sans-serif' }}>
<h1>Next.js + NestJS + PostgreSQL + Redis</h1>
{error && <p>Помилка: {error}</p>}
{!status && !error && <p>Завантаження...</p>}
{status && (
<pre>{JSON.stringify(status, null, 2)}</pre>
)}
</main>
);
}Браузер виконує цей запит із хостової системи, тому значення NEXT_PUBLIC_API_URL має бути:
http://localhost:3001Не потрібно використовувати http://api:3001 у клієнтському коді. Ім’я api доступне лише іншим контейнерам у Docker-мережі, але не браузеру користувача.
Файл api/Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3001
CMD ["npm", "run", "start:dev"]Цей образ:
використовує Node.js на базі Alpine Linux;
встановлює залежності;
копіює код застосунку;
запускає NestJS у режимі спостереження за змінами.
Файл web/Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]Для Next.js режим розробки використовує next dev.
Файл compose.yaml містить спільну конфігурацію сервісів:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app_password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
api:
build:
context: ./api
dockerfile: Dockerfile
environment:
PORT: 3001
DATABASE_URL: postgresql://app:app_password@postgres:5432/app
REDIS_URL: redis://redis:6379
ports:
- "3001:3001"
networks:
- backend
- frontend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
web:
build:
context: ./web
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: http://localhost:3001
environment:
NEXT_PUBLIC_API_URL: http://localhost:3001
ports:
- "3000:3000"
networks:
- frontend
depends_on:
- api
volumes:
postgres_data:
redis_data:
networks:
backend:
driver: bridge
frontend:
driver: bridgeМи використовуємо дві мережі:
backend — PostgreSQL, Redis і NestJS;
frontend — NestJS і Next.js.
Таким чином:
Next.js не має прямого доступу до PostgreSQL;
Next.js не має прямого доступу до Redis;
NestJS підключений до обох мереж;
API може звертатися до бази даних за іменем postgres;
API може звертатися до Redis за іменем redis.
Публікувати порти PostgreSQL і Redis назовні не потрібно. Вони доступні API через внутрішню мережу.
Томи:
volumes:
postgres_data:
redis_data:потрібні для збереження даних після перезапуску або видалення контейнерів.
Якщо видалити контейнери командою:
docker compose downдані в томах залишаться.
Щоб видалити також томи:
docker compose down -vЦю команду слід використовувати обережно, оскільки вона видаляє дані PostgreSQL і Redis.
Файл compose.dev.yaml додає bind mounts і налаштовує запуск у режимі розробки:
services:
api:
volumes:
- ./api:/app
- api_node_modules:/app/node_modules
command: npm run start:dev
web:
volumes:
- ./web:/app
- web_node_modules:/app/node_modules
- web_next:/app/.next
command: npm run dev
volumes:
api_node_modules:
web_node_modules:
web_next:Bind mount:
- ./api:/appсинхронізує локальний код із контейнером. Зміни у файлах одразу стають доступними процесу NestJS.
Окремий том для node_modules потрібен для того, щоб bind mount не приховав залежності, встановлені під час побудови образу.
Запуск режиму розробки:
docker compose -f compose.yaml -f compose.dev.yaml up --buildПісля запуску:
Next.js буде доступний на http://localhost:3000;
NestJS буде доступний на http://localhost:3001/status;
PostgreSQL і Redis будуть доступні лише з контейнерів у мережі backend.
Для запуску у фоновому режимі:
docker compose -f compose.yaml -f compose.dev.yaml up --build -dДля перегляду логів:
docker compose -f compose.yaml -f compose.dev.yaml logs -fДля перегляду логів лише API:
docker compose -f compose.yaml -f compose.dev.yaml logs -f apiЗупинка контейнерів:
docker compose -f compose.yaml -f compose.dev.yaml downФайл api/Dockerfile.prod:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3001
CMD ["node", "dist/main"]Тут використовуються два етапи:
build містить TypeScript і dev-залежності та компілює застосунок;
production містить лише production-залежності й скомпільований каталог dist.
Це зменшує розмір фінального образу та не переносить у нього вихідні TypeScript-файли.
У web/next.config.js увімкніть standalone-збірку:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;Файл web/Dockerfile.prod:
FROM node:22-alpine AS build
WORKDIR /app
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=build /app/public ./public
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Значення NEXT_PUBLIC_API_URL використовується під час збірки Next.js. Для змінних із префіксом NEXT_PUBLIC_ це важливо: вони потрапляють у клієнтський JavaScript-код під час npm run build.
Файл compose.prod.yaml перевизначає Dockerfile і команди:
services:
api:
build:
context: ./api
dockerfile: Dockerfile.prod
environment:
NODE_ENV: production
command: node dist/main
web:
build:
context: ./web
dockerfile: Dockerfile.prod
args:
NEXT_PUBLIC_API_URL: http://localhost:3001
environment:
NODE_ENV: production
command: node server.jsЗапуск production-режиму:
docker compose -f compose.yaml -f compose.prod.yaml up --build -dУ цьому режимі:
API запускається зі скомпільованого dist;
Next.js запускається зі standalone-збірки;
вихідний код не монтується в контейнери;
dev-сервери та hot reload не використовуються;
дані PostgreSQL і Redis зберігаються в іменованих томах.
Перевірка контейнерів:
docker compose -f compose.yaml -f compose.prod.yaml psПеревірка API:
curl http://localhost:3001/statusОчікуваний результат матиме приблизно такий вигляд:
{
"api": "ok",
"postgres": {
"connected": true,
"serverTime": "2026-09-02T10:00:00.000Z"
},
"redis": {
"connected": true,
"lastStatusCheck": "2026-09-02T10:00:00.000Z"
}
}Фактичне значення часу буде іншим.
Отримати список контейнерів:
docker compose psВідкрити shell у контейнері API:
docker compose exec api shІмена postgres і redis можна перевірити з контейнера API:
getent hosts postgres
getent hosts redisУсередині контейнера API адреса localhost означає сам контейнер API. Тому такі адреси неправильні:
postgresql://app:app_password@localhost:5432/app
redis://localhost:6379Правильні адреси використовують імена Compose-сервісів:
postgresql://app:app_password@postgres:5432/app
redis://redis:6379Перегляд томів:
docker volume lsПерегляд мереж:
docker network lsЗупинка контейнерів без видалення:
docker compose stopПовторний запуск:
docker compose startВидалення контейнерів і мереж, але зі збереженням даних:
docker compose downПовне очищення разом із даними:
docker compose down -vПримусова перебудова образів без використання кешу:
docker compose build --no-cachelocalhost для PostgreSQL або RedisУ контейнері localhost посилається на поточний контейнер, а не на хост і не на інший сервіс.
Використовуйте:
postgres
redisяк імена хостів.
Самого depends_on недостатньо, якщо не вказати умову готовності. Контейнер PostgreSQL може бути вже запущений, але ще не приймати з’єднання.
У базовій конфігурації використовується:
depends_on:
postgres:
condition: service_healthyА готовність визначається через healthcheck.
node_modulesЯкщо використати лише:
volumes:
- ./api:/appлокальний каталог може приховати /app/node_modules, встановлений в образі.
Тому для залежностей використовуємо окремий іменований том:
volumes:
- ./api:/app
- api_node_modules:/app/node_modulesДля зв’язку між контейнерами не потрібні такі налаштування:
ports:
- "5432:5432"або:
ports:
- "6379:6379"API підключається до них через внутрішню мережу. Публікувати ці порти варто лише тоді, коли до бази або Redis потрібно підключатися безпосередньо з хостової системи.
api у браузеріАдреса:
http://api:3001працює для контейнерів у Docker-мережі, але не для браузера. Браузер має звертатися до опублікованого порту хоста:
http://localhost:3001down -vКоманда:
docker compose down -vвидаляє томи разом із даними. У production-середовищі не використовуйте її без потреби.
NEXT_PUBLIC_API_URL після production-збіркиЗначення NEXT_PUBLIC_API_URL вбудовується у клієнтський код під час npm run build. Зміна змінної лише в секції environment після збірки не змінить уже створений JavaScript-код.
Для іншого API потрібно перебудувати образ із новим build argument.
У результаті ми зібрали повний стек у Docker Compose:
Next.js працює в окремому контейнері;
NestJS надає API в окремому контейнері;
PostgreSQL зберігає дані в іменованому томі;
Redis використовує окремий іменований том;
API підключається до сервісів через імена postgres і redis;
мережа backend ізолює базу даних і Redis;
мережа frontend з’єднує Next.js із NestJS;
compose.dev.yaml вмикає розробку з bind mounts і hot reload;
compose.prod.yaml використовує оптимізовані production-образи;
healthcheck гарантує, що API запускається після готовності PostgreSQL і Redis.