Как делать11 октября 2026 г.6 мин чтения
Docker и деплойШаг 4 из 8

Docker Compose: приложение и база локально одной командой

Коротко

Docker Compose описывает несколько контейнеров в одном файле compose.yaml: сервисы, порты, тома, сеть и переменные окружения — и поднимает их командой docker compose up. Чтобы API не стартовал раньше базы, у PostgreSQL и Redis задают healthcheck, а у API — depends_on с condition: service_healthy. Секреты берутся из .env, данные живут в именованных томах и переживают docker compose down, если не добавить -v.

Пошаговый урокУровень: СреднийDockerПрактика: ~1,5–3 ч

После материала вы сможете

  • описать в compose.yaml API, PostgreSQL и Redis с портами, именованными томами и политикой перезапуска
  • объяснить, как сервисы находят друг друга по именам во внутренней сети и чем порт контейнера отличается от порта хоста
  • передать настройки через .env с интерполяцией ${…}, значениями по умолчанию и обязательными переменными и проверить итог docker compose config
  • настроить healthcheck и depends_on с service_healthy и service_completed_successfully, вынести dev-настройки в override и профили

Проверено на

Docker Compose 5.3 (Docker Desktop, Docker Engine 29.6)PostgreSQL 17.11 (postgres:17-alpine)Redis 8.10 (redis:8.10-alpine)Node.js 24.21 (node:24.21.0-alpine)Express 5.3.0pg 8.23.1node-redis 6.3.0

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Зачем Compose: три контейнера одной командой
  2. 02Приложение: API с PostgreSQL и Redis
  3. 03compose.yaml: сервисы, порты, тома
  4. 04Сеть и имена сервисов
  5. 05Переменные окружения и .env
  6. 06healthcheck и depends_on: порядок запуска
  7. 07Профили и override для разработки
  8. 08Команды на каждый день и упражнения

Зачем Compose: три контейнера одной командой

В уроке «Docker с нуля» мы запускали один контейнер командой docker run с портом, переменными и томом. Реальному API нужны ещё база и кэш, а значит — три длинные команды, общая сеть, тома, пароли и правильный порядок запуска. Compose описывает всё это декларативно в compose.yaml, и окружение поднимается одинаково у каждого разработчика.

  1. 01curl / браузер
  2. 02api :3000
  3. 03db: PostgreSQL :5432
  4. 04cache: Redis :6379
Проект урока: API на Node.js ходит в PostgreSQL за данными и в Redis за кэшем — по именам сервисов во внутренней сети.

Проект

Что это в Compose
Всё, что описано в одном compose.yaml; имя по умолчанию — папка, у нас notes

Сервис

Что это в Compose
Описание контейнера: образ или сборка, порты, переменные, тома; api, db, cache

Контейнер

Что это в Compose
Запущенный экземпляр сервиса: notes-api-1

Сеть

Что это в Compose
Создаётся автоматически (notes_default), сервисы находят друг друга по имени

Том

Что это в Compose
Именованное хранилище данных проекта: notes_pgdata, notes_redisdata

Compose входит в Docker Desktop, на Linux ставится как плагин docker compose. Проверено на Docker Compose 5.3 с Docker Engine 29.6. Поле version в начале файла больше не нужно: по документации оно только информационное, и Compose предупреждает, что оно устарело.

Приложение: API с PostgreSQL и Redis

Возьмём API заметок из урока о Docker и подключим к нему настоящие хранилища: заметки — в PostgreSQL, список заметок кэшируется в Redis на 30 секунд по схеме cache-aside. /health проверяет и базу, и Redis — его будет вызывать HEALTHCHECK контейнера. Адреса хранилищ приходят из переменных DATABASE_URL и REDIS_URL.

bash
mkdir notes && cd notes
npm init -y && npm pkg set type=module
npm install express@5.3.0 pg@8.23.1 redis@6.3.0
npm install -D typescript@7.0.2 @types/express@5.0.6 @types/node@24.19.2 @types/pg@8.23.1
Проект и зависимости; версии — актуальные в npm на 11.10.2026.
src/server.ts
import express from "express";
import pg from "pg";
import { createClient } from "redis";

const port = Number(process.env.PORT ?? 3000);
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const redis = createClient({ url: process.env.REDIS_URL });
redis.on("error", (err) => console.error("redis error:", err.message));
await redis.connect();

const CACHE_KEY = "notes:all";
const app = express();
app.use(express.json());

app.get("/health", async (_req, res) => {
  try {
    await pool.query("SELECT 1");
    await redis.ping();
    res.json({ status: "ok", db: "ok", redis: "ok" });
  } catch (err) {
    res.status(503).json({ status: "error", message: (err as Error).message });
  }
});

// Cache-aside: read from Redis, on a miss read from PostgreSQL and cache for 30 s
app.get("/api/notes", async (_req, res) => {
  const cached = await redis.get(CACHE_KEY);
  if (cached) {
    res.set("X-Cache", "HIT").type("json").send(cached);
    return;
  }
  const { rows } = await pool.query("SELECT id, text, created_at FROM notes ORDER BY id");
  await redis.set(CACHE_KEY, JSON.stringify(rows), { EX: 30 });
  res.set("X-Cache", "MISS").json(rows);
});

app.post("/api/notes", async (req, res) => {
  const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
  if (!text) {
    res.status(400).json({ error: "text is required" });
    return;
  }
  const { rows } = await pool.query(
    "INSERT INTO notes (text) VALUES ($1) RETURNING id, text, created_at",
    [text],
  );
  await redis.del(CACHE_KEY);
  res.status(201).json(rows[0]);
});

const server = app.listen(port, () => {
  console.log(`notes-api listening on port ${port}`);
});

process.on("SIGTERM", () => {
  console.log("SIGTERM received, shutting down");
  server.close(async () => {
    await pool.end();
    await redis.close();
    process.exit(0);
  });
});
Проверено: tsc без ошибок, работа в контейнере с PostgreSQL 17.11 и Redis 8.10.
db/init.sql
-- Runs once, when the PostgreSQL data volume is empty
CREATE TABLE notes (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  text       text        NOT NULL CHECK (length(text) BETWEEN 1 AND 500),
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO notes (text) VALUES ('Hello from init.sql');
Скрипты из /docker-entrypoint-initdb.d образ postgres выполняет только при пустом каталоге данных — то есть при первом запуске с новым томом.

Dockerfile — тот же многоэтапный из урока «Docker с нуля», без двух строк про папку данных (DATA_DIR и /app/data): файлы приложение больше не пишет. Его HEALTHCHECK вызывает /health, поэтому контейнер API станет healthy только когда доступны и база, и Redis.

compose.yaml: сервисы, порты, тома

compose.yaml
services:
  api:
    build: .
    ports:
      - "127.0.0.1:${API_PORT:-8080}:3000"
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
        restart: true
      cache:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped

  cache:
    image: redis:8.10-alpine
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  psql:
    image: postgres:17-alpine
    profiles: ["tools"]
    environment:
      PGHOST: db
      PGUSER: ${POSTGRES_USER}
      PGPASSWORD: ${POSTGRES_PASSWORD}
      PGDATABASE: ${POSTGRES_DB}
    entrypoint: ["psql"]
    depends_on:
      db:
        condition: service_healthy

volumes:
  pgdata:
  redisdata:
Проверено: Docker Compose 5.3.1, docker compose up -d --wait — все три сервиса healthy.

build: .

Что делает
Собрать образ из Dockerfile в папке проекта; имя образа — notes-api

image: postgres:17-alpine

Что делает
Готовый образ из реестра; версию закрепляем, как и в уроке «PostgreSQL: установка в Docker»

ports: "127.0.0.1:${API_PORT:-8080}:3000"

Что делает
Порт хоста → порт контейнера; без 127.0.0.1 порт открыт на всех интерфейсах

volumes: pgdata:/var/lib/postgresql/data

Что делает
Именованный том для данных; для PostgreSQL 17 путь именно такой, у 18 — /var/lib/postgresql

./db/init.sql:…:ro

Что делает
Файл с хоста (bind mount) только для чтения

command:

Что делает
Заменяет CMD образа: Redis с журналом AOF (--appendonly yes), чтобы данные переживали перезапуск

restart: unless-stopped

Что делает
Перезапускать после сбоя и перезагрузки Docker, если контейнер не остановили вручную

volumes: в конце файла

Что делает
Объявление именованных томов проекта

Сеть и имена сервисов

Compose создаёт для проекта сеть <проект>_default и подключает к ней все сервисы. Внутри сети имя сервиса работает как имя хоста: поэтому в DATABASE_URL написано @db:5432, а в REDIS_URL — cache:6379. Сервисы общаются по портам контейнеров; порт хоста (8080, 55442) нужен только для подключения снаружи, с вашей машины.

terminal
$ curl -s http://127.0.0.1:8080/health
{"status":"ok","db":"ok","redis":"ok"}
$ curl -si http://127.0.0.1:8080/api/notes | grep -E "^X-Cache|^\["
X-Cache: MISS
[{"id":"1","text":"Hello from init.sql","created_at":"2026-10-12T02:08:11.573Z"}]
$ curl -si http://127.0.0.1:8080/api/notes | grep -E "^X-Cache|^\["
X-Cache: HIT
[{"id":"1","text":"Hello from init.sql","created_at":"2026-10-12T02:08:11.573Z"}]
$ curl -s -X POST http://127.0.0.1:8080/api/notes -H "Content-Type: application/json" -d '{"text":"Order coffee beans"}'
{"id":"2","text":"Order coffee beans","created_at":"2026-10-12T02:08:29.159Z"}
$ curl -si http://127.0.0.1:8080/api/notes | grep -E "^X-Cache|^\["
X-Cache: MISS
[{"id":"1","text":"Hello from init.sql","created_at":"2026-10-12T02:08:11.573Z"},{"id":"2","text":"Order coffee beans","created_at":"2026-10-12T02:08:29.159Z"}]
Проверка API: первый запрос — промах кэша, второй — попадание, после POST кэш сброшен. Реальный вывод.
terminal
$ docker compose exec api node -e "require('node:dns').promises.lookup('db').then(a => console.log('db ->', a.address))"
db -> 192.168.0.3
$ docker network ls --filter name=notes
NETWORK ID     NAME               DRIVER    SCOPE
392a30dbb8d0   notes_default      bridge    local
Имя db внутри сети разрешается в адрес контейнера; сеть называется по проекту. В проверке у проекта был префикс kb-, в статье он убран.
  • id пришёл строкой ("1"): тип bigint драйвер pg по умолчанию отдаёт строкой, чтобы не потерять точность больших чисел в JavaScript
  • localhost внутри контейнера — это сам контейнер. Ошибка «API не видит базу» чаще всего — localhost вместо имени сервиса в строке подключения
  • Сеть можно описать явно в разделе networks — например, чтобы отделить базу от публичных сервисов. Для одного проекта хватает сети по умолчанию

Переменные окружения и .env

Файл .env рядом с compose.yaml Compose читает автоматически и подставляет значения в выражения ${…} в самом файле. Это интерполяция: значения попадают в контейнер, только если вы передали их через environment. В git кладут .env.example без секретов, а сам .env — в .gitignore.

.env
# Local development only. Copy from .env.example; never commit this file
POSTGRES_USER=notes
POSTGRES_PASSWORD=change-me-locally
POSTGRES_DB=notes
API_PORT=8080
Пароль — только для локальной разработки. Если в пароле есть @, : или /, в DATABASE_URL его нужно закодировать (URL-encoding).
.env.example
POSTGRES_USER=notes
POSTGRES_PASSWORD=
POSTGRES_DB=notes
API_PORT=8080
Шаблон для коллег: этот файл коммитится.

${POSTGRES_USER}

Значение
Значение переменной; если её нет — пустая строка и предупреждение

${API_PORT:-8080}

Значение
Значение или 8080, если переменная не задана или пустая

${POSTGRES_PASSWORD:?текст}

Значение
Остановиться с ошибкой и текстом, если переменная не задана или пустая

$${POSTGRES_USER}

Значение
Двойной $ — не подставлять: переменную раскроет оболочка внутри контейнера
terminal
$ docker compose config --services
cache
db
api
$ docker compose --profile tools config --services
cache
db
api
psql
$ docker compose config | grep -E "DATABASE_URL|POSTGRES_PASSWORD"
      DATABASE_URL: postgresql://notes:change-me-locally@db:5432/notes
      POSTGRES_PASSWORD: change-me-locally
$ mv .env .env.bak; docker compose config > /dev/null; mv .env.bak .env
time="2026-10-11T20:07:56-06:00" level=warning msg="The \"POSTGRES_USER\" variable is not set. Defaulting to a blank string."
time="2026-10-11T20:07:56-06:00" level=warning msg="The \"POSTGRES_PASSWORD\" variable is not set. Defaulting to a blank string."
time="2026-10-11T20:07:56-06:00" level=warning msg="The \"POSTGRES_DB\" variable is not set. Defaulting to a blank string."
error while interpolating services.db.environment.POSTGRES_USER: required variable POSTGRES_USER is missing a value: set POSTGRES_USER in .env
docker compose config печатает итоговую конфигурацию после подстановки — первое, что стоит смотреть при странном поведении. Без .env срабатывает :?.

Приоритет источников: переменная оболочки сильнее файла из --env-file, а тот сильнее .env. Поэтому порт можно поменять на один запуск, не трогая файлы:

terminal
$ DB_PORT=55443 docker compose config | grep published
        published: "8080"
        published: "56392"
        published: "55443"
DB_PORT из командной строки перекрыл значение из .env.

healthcheck и depends_on: порядок запуска

Compose всегда запускает и останавливает контейнеры в порядке зависимостей, но по умолчанию ждёт только, пока зависимость запущена, а не пока она готова. PostgreSQL после старта контейнера ещё несколько секунд инициализирует базу. Поэтому у db и cache есть healthcheck (pg_isready и redis-cli ping), а api зависит от них с condition: service_healthy. Флаг --wait заставляет саму команду дождаться, пока все сервисы станут healthy.

terminal
$ docker compose up -d --wait
 Image notes-api Building
 Image notes-api Built
 Network notes_default Creating
 Network notes_default Created
 Volume notes_redisdata Creating
 Volume notes_redisdata Created
 Volume notes_pgdata Creating
 Volume notes_pgdata Created
 Container notes-cache-1 Creating
 Container notes-db-1 Creating
 Container notes-cache-1 Created
 Container notes-db-1 Created
 Container notes-api-1 Creating
 Container notes-api-1 Created
 Container notes-cache-1 Starting
 Container notes-db-1 Starting
 Container notes-cache-1 Started
 Container notes-db-1 Started
 Container notes-cache-1 Waiting
 Container notes-db-1 Waiting
 Container notes-cache-1 Healthy
 Container notes-db-1 Healthy
 Container notes-api-1 Starting
 Container notes-api-1 Started
 Container notes-db-1 Waiting
 Container notes-cache-1 Waiting
 Container notes-api-1 Waiting
 Container notes-db-1 Healthy
 Container notes-cache-1 Healthy
 Container notes-api-1 Healthy
$ docker compose ps
NAME               IMAGE                COMMAND                  SERVICE   CREATED          STATUS                    PORTS
notes-api-1        notes-api            "docker-entrypoint.s…"   api       12 seconds ago   Up 5 seconds (healthy)    127.0.0.1:8080->3000/tcp
notes-cache-1      redis:8.10-alpine    "docker-entrypoint.s…"   cache     12 seconds ago   Up 11 seconds (healthy)   127.0.0.1:56392->6379/tcp
notes-db-1         postgres:17-alpine   "docker-entrypoint.s…"   db        12 seconds ago   Up 11 seconds (healthy)   127.0.0.1:55442->5432/tcp
$ curl -s http://127.0.0.1:8080/health
{"status":"ok","db":"ok","redis":"ok"}
Чистый запуск: сначала cache и db до Healthy, только потом api. Строки сборки образа опущены.

Что будет с короткой записью depends_on: [db, cache]: мы запустили тот же проект с ней и опрашивали /health раз в полсекунды.

terminal
$ docker compose -f compose.short.yaml up -d
 Network notes_default Creating
 Network notes_default Created
 Volume notes_pgdata Creating
 Volume notes_pgdata Created
 Volume notes_redisdata Creating
 Volume notes_redisdata Created
 Container notes-db-1 Creating
 Container notes-cache-1 Creating
 Container notes-cache-1 Created
 Container notes-db-1 Created
 Container notes-api-1 Creating
 Container notes-api-1 Created
 Container notes-cache-1 Starting
 Container notes-db-1 Starting
 Container notes-cache-1 Started
 Container notes-db-1 Started
 Container notes-api-1 Starting
 Container notes-api-1 Started
$ for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w "%{http_code} " http://127.0.0.1:8080/health; sleep 0.5; done; echo
000 503 200 200 200 200
$ docker compose -f compose.short.yaml logs api
api-1  | notes-api listening on port 3000
$ docker compose -f compose.short.yaml ps --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
api       Up 4 seconds (health: starting)
cache     Up 4 seconds (health: starting)
db        Up 4 seconds (health: starting)
Реальный вывод: 000 — API ещё не слушает порт, 503 — API запущен, а база ещё не готова; через секунду всё работает. На медленной машине или с миграциями окно ошибок больше.

service_started

Когда стартует зависимый сервис
Когда контейнер зависимости запущен (так работает короткая запись)

service_healthy

Когда стартует зависимый сервис
Когда healthcheck зависимости прошёл

service_completed_successfully

Когда стартует зависимый сервис
Когда зависимость завершилась с кодом 0 — для миграций и подготовки данных

restart: true в depends_on перезапускает API, когда Compose перезапускает или обновляет базу:

terminal
$ docker compose restart db
 Container notes-db-1 Restarting
 Container notes-db-1 Started
 Container notes-db-1 Waiting
 Container notes-db-1 Healthy
 Container notes-api-1 Restarting
 Container notes-api-1 Started
$ docker compose ps --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
api       Up Less than a second (health: starting)
cache     Up 36 seconds (healthy)
db        Up 6 seconds (healthy)
$ docker compose logs api --tail 3
api-1  | notes-api listening on port 3000
api-1  | SIGTERM received, shutting down
api-1  | notes-api listening on port 3000
$ curl -s http://127.0.0.1:8080/health
{"status":"ok","db":"ok","redis":"ok"}
После restart db Compose дождался Healthy и перезапустил api; в логе видно корректное завершение по SIGTERM.

Профили и override для разработки

Кроме compose.yaml, Compose автоматически читает compose.override.yaml из той же папки и объединяет их: списки портов складываются, переменные объединяются по имени, одиночные значения из второго файла заменяют первые. Туда удобно вынести то, что нужно только разработчику, — например, порты базы и Redis для GUI-клиента.

compose.override.yaml
# Loaded automatically by "docker compose up": local development extras
services:
  db:
    ports:
      - "127.0.0.1:${DB_PORT:-5432}:5432"
  cache:
    ports:
      - "127.0.0.1:${REDIS_PORT:-6379}:6379"
Порт по умолчанию — стандартный; если он занят, задайте DB_PORT/REDIS_PORT в .env.
terminal
$ docker compose config | grep -E "^  [a-z]+:$|published"
  api:
        published: "8080"
  cache:
        published: "56392"
  db:
        published: "55442"
  default:
  pgdata:
  redisdata:
$ docker compose -f compose.yaml config | grep -E "^  [a-z]+:$|published"
  api:
        published: "8080"
  cache:
  db:
  default:
  pgdata:
  redisdata:
С override опубликованы порты всех трёх сервисов, с явным -f compose.yaml — только API. Так запускают окружение без dev-добавок.

Профили включают сервисы по запросу. Сервис без profiles запускается всегда, с профилем — только если профиль активен (--profile tools или COMPOSE_PROFILES=tools) или сервис вызван явно. У нас так описан psql — клиент базы, который не нужен в обычной работе:

terminal
$ docker compose run --rm psql -c "\dt"
 Container notes-db-1 Running
 Container notes-db-1 Waiting
 Container notes-db-1 Healthy
 Container notes-psql-run-fa3cc8af018b Creating
 Container notes-psql-run-fa3cc8af018b Created
       List of relations
 Schema | Name  | Type  | Owner
--------+-------+-------+-------
 public | notes | table | notes
(1 row)
run --rm создаёт одноразовый контейнер сервиса, дожидается здоровой базы и удаляет контейнер после выхода.
terminal
$ docker compose up -d --wait
 Image notes-api Building
 …
 Container notes-db-1 Starting
 Container notes-cache-1 Starting
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint notes-cache-1 (bc682bd84ba0…): Bind for 0.0.0.0:6379 failed: port is already allocated
$ printf 'DB_PORT=55442\nREDIS_PORT=56392\n' >> .env
$ docker compose up -d --wait
 Container notes-cache-1 Recreate
 Container notes-db-1 Recreate
 …
 Container notes-api-1 Healthy
Реальный вывод, длинные части сокращены (…).

Команды на каждый день и упражнения

docker compose up -d --wait

Что делает
Собрать при необходимости, запустить в фоне и дождаться healthy

docker compose up -d --build

Что делает
Пересобрать образы перед запуском — после изменения кода

docker compose ps

Что делает
Контейнеры проекта и их статус

docker compose logs -f api

Что делает
Логи сервиса в реальном времени; без имени — всех сервисов

docker compose exec db psql -U notes

Что делает
Команда в работающем контейнере сервиса

docker compose run --rm psql -c …

Что делает
Одноразовый контейнер сервиса

docker compose restart db

Что делает
Перезапустить сервис

docker compose down

Что делает
Удалить контейнеры и сеть; тома остаются

docker compose down -v

Что делает
То же плюс тома проекта — данные будут потеряны
terminal
$ docker compose down
 …
 Container notes-api-1 Removed
 …
 Container notes-db-1 Removed
 …
 Container notes-cache-1 Removed
 …
 Network notes_default Removed
$ docker volume ls --filter name=notes
DRIVER    VOLUME NAME
local     notes_pgdata
local     notes_redisdata
$ docker compose up -d --wait
 …
 Container notes-cache-1 Healthy
 Container notes-db-1 Healthy
 …
 Container notes-cache-1 Healthy
 Container notes-db-1 Healthy
 Container notes-api-1 Healthy
$ docker compose logs db | grep -iE "skipping|init.sql"
db-1  | PostgreSQL Database directory appears to contain a database; Skipping initialization
$ curl -s http://127.0.0.1:8080/api/notes
[{"id":"1","text":"Hello from init.sql","created_at":"2026-10-12T02:08:11.573Z"},{"id":"2","text":"Order coffee beans","created_at":"2026-10-12T02:08:29.159Z"}]
$ docker compose down -v
 …
 Container notes-api-1 Removed
 …
 Container notes-db-1 Removed
 …
 Container notes-cache-1 Removed
 …
 Volume notes_pgdata Removed
 Volume notes_redisdata Removed
 Network notes_default Removed
$ docker volume ls --filter name=notes
DRIVER    VOLUME NAME
down сохранил тома — после up база не инициализировалась заново и заметки на месте; down -v удалил тома. Реальный вывод, однотипные строки прогресса свёрнуты (…).

Дальше — секреты и переменные окружения в продакшене и деплой: как этот же проект запускается на сервере, где .env с паролями быть не должно.

Как проверено11 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    src/server.ts (Express 5.3.0, pg 8.23.1, node-redis 6.3.0), установка зависимостей командами из статьи

    npm init/npm pkg set/npm install в чистой папке, tsc --noEmit (TypeScript 7.0.2) без ошибок; redis.close() вместо устаревшего quit() (по d.ts node-redis 6.3.0)

  • запущено, работает

    db/init.sql, compose.yaml, compose.override.yaml, .env, .env.example

    Docker Compose 5.3.1 + Docker Engine 29.6.2 (Docker Desktop): docker compose up -d --wait с нуля — cache и db Healthy, затем api Healthy; /health 200; init.sql выполнен (лог entrypoint). Dockerfile — из урока docker-osnovy без строк DATA_DIR и /app/data

  • запущено, работает

    config: --services, --profile tools, интерполяция, ошибка :? без .env и без POSTGRES_PASSWORD, приоритет переменной оболочки (DB_PORT=55443)

    реальный вывод docker compose config; в статье команда grep сокращена до DATABASE_URL|POSTGRES_PASSWORD (в прогоне шаблон включал ещё 127.0.0.1 — те же строки плюс host_ip)

  • запущено, работает

    конфликт порта 6379 при первом up и исправление через DB_PORT/REDIS_PORT в .env

    на машине проверки 5432 и 6379 заняты чужими контейнерами: Bind for 0.0.0.0:6379 failed: port is already allocated; после добавления портов up пересоздал контейнеры. ID эндпоинта в статье сокращён

  • запущено, работает

    кэш X-Cache MISS/HIT/инвалидация, redis-cli TTL, exec psql, run --rm psql (профиль tools), DNS-имя db, docker network ls, logs

    реальный вывод; id bigint возвращается строкой (поведение pg по умолчанию)

  • запущено, работает

    короткая запись depends_on

    копия compose.yaml с depends_on: [db, cache] и пустыми томами: /health раз в 0.5 с — 000 503 200 200 200 200

  • запущено, работает

    restart db с restart: true, down (тома сохранились, Skipping initialization), down -v

    api перезапущен после Healthy базы, в логе SIGTERM received; после down/up заметки на месте; после down -v томов нет. Однотипные строки прогресса в статье свёрнуты в «…»

  • запущено, работает

    override: config с override и с -f compose.yaml

    с override опубликованы 8080/56392/55442, с -f compose.yaml — только 8080

  • запущено, работает

    упражнение modify (POSTGRES_PASSWORD без значения)

    required variable POSTGRES_PASSWORD is missing a value: set POSTGRES_PASSWORD in .env

  • запущено, работает

    упражнение apply (сервис migrate + service_completed_successfully)

    полный compose.yaml с фрагментом из решения: при чистом запуске migrate Exited (0) до старта api, \d notes показывает pinned, повторный up — NOTICE … skipping, --wait exit 0

  • запущено, работает

    упражнение repeat

    шаги совпадают с проверенным основным сценарием

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.