Docker Compose: как поднять приложение и базу
Соберём локальный стенд из Node.js-приложения и PostgreSQL 16: база будет запускаться раньше приложения, данные сохранятся после перезапуска, а проверка займёт несколько команд.

За 10–15 минут вы поднимете два контейнера: приложение на Node.js 22 и PostgreSQL 16. Они будут находить друг друга по имени сервиса, база дождётся готовности перед запуском приложения, а данные останутся в Docker volume после пересоздания контейнеров.
Что подготовить до начала
- Docker Desktop с Docker Compose v2 или Docker Engine с Compose v2.
- Пустую папку проекта с правом создавать файлы.
- Свободные порты 3000 и 5432. Порт 5432 нужен только для подключения к базе с хоста.
- Терминал. Node.js на компьютере не требуется: приложение запустится внутри контейнера.
Версии примера: Docker Compose v2, Node.js 22, Express 4.21.2 и PostgreSQL 16. Команда Compose запускается как docker compose, без отдельного дефиса.
Сноска. Healthcheck — проверка, которая сообщает Docker, готов ли сервис принимать подключения. Состояние контейнера «запущен» ещё не означает, что PostgreSQL уже принимает запросы.
Главная ошибка в связке «приложение плюс база» — считать, что
depends_onпроверяет готовность базы. Сам по себе он задаёт порядок запуска, но не ждёт успешного подключения.
Шаг 1. Создайте структуру проекта
Создайте каталог и четыре файла. В результате дерево должно выглядеть так:
compose-demo/
├── compose.yaml
├── Dockerfile
├── package.json
└── server.js
В package.json зафиксируйте зависимости приложения:
{
"name": "compose-demo",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "4.21.2",
"pg": "8.13.1"
}
}
Проверка: команда ls или просмотр папки показывает все четыре файла. На Windows проверьте, что файлы не получили двойное расширение вроде server.js.txt.
Шаг 2. Добавьте приложение
Файл server.js создаёт таблицу при старте, записывает посещение и возвращает количество записей. Подключение берётся из переменной DATABASE_URL, поэтому адрес базы не зашит в код.
const express = require('express');
const { Pool } = require('pg');
const app = express();
const port = Number(process.env.PORT || 3000);
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
async function start() {
await pool.query(`
CREATE TABLE IF NOT EXISTS visits (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
app.get('/', async (request, response) => {
await pool.query('INSERT INTO visits DEFAULT VALUES');
const result = await pool.query('SELECT COUNT(*)::int AS count FROM visits');
response.json({ message: 'Compose works', visits: result.rows[0].count });
});
app.get('/health', async (request, response) => {
await pool.query('SELECT 1');
response.json({ status: 'ok' });
});
app.listen(port, '0.0.0.0', () => {
console.log(`Application is listening on port ${port}`);
});
}
start().catch((error) => {
console.error('Application failed to start:', error.message);
process.exit(1);
});
Проверка будет на следующем шаге: после запуска запрос к /health должен вернуть JSON со статусом ok.
Шаг 3. Опишите образ приложения
В Dockerfile используйте официальный образ Node.js 22 Alpine. Сначала копируются манифесты зависимостей, затем исходный код. Это позволяет Docker использовать кеш для слоя с npm install, если меняется только приложение.
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]
Проверка: Dockerfile лежит рядом с compose.yaml, а строка CMD запускает существующий скрипт из package.json.
Шаг 4. Свяжите контейнеры в Compose
Создайте compose.yaml. Переменная POSTGRES_PASSWORD обязательна: задайте её перед запуском. В примере используется только заглушка, а не настоящий пароль.
services:
db:
image: postgres:16
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD first}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 3s
timeout: 3s
retries: 10
app:
build: .
environment:
PORT: 3000
DATABASE_URL: postgresql://app_user:${POSTGRES_PASSWORD}@db:5432/app_db
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
volumes:
postgres_data:
Задайте переменную в текущем терминале. В Bash и PowerShell синтаксис отличается:
# Bash, macOS и Linux
export POSTGRES_PASSWORD=YOUR_DB_PASSWORD
# PowerShell
$env:POSTGRES_PASSWORD = "YOUR_DB_PASSWORD"
Здесь db — имя сервиса и одновременно DNS-имя контейнера внутри Compose-сети. Поэтому в строке подключения нельзя использовать localhost: внутри контейнера это адрес самого приложения, а не PostgreSQL.
Проверка: команда docker compose config должна вывести объединённую конфигурацию без сообщения об отсутствующей переменной. Если команда показывает пароль в терминале, не сохраняйте этот вывод в публичный лог.
Шаг 5. Запустите стенд
- Соберите образ и запустите сервисы:
docker compose up --build -d. Ключ--buildнужен, чтобы Docker собрал образ приложения после изменений в Dockerfile или package.json. - Проверьте состояние:
docker compose ps. У сервисаdbдолжно появиться состояниеhealthy, а уapp— запущенный контейнер. - Посмотрите логи приложения:
docker compose logs app. В конце должна быть строкаApplication is listening on port 3000. - Откройте адрес: перейдите на http://localhost:3000/. Ответ будет содержать
"message":"Compose works"и число посещений. - Проверьте сохранение данных: обновите страницу несколько раз, затем выполните
docker compose restart. Счётчик продолжит расти, потому что данные лежат в именованном volume.
Если не получилось
«Bind for 0.0.0.0:3000 failed: port is already allocated»
Порт 3000 занят другим процессом. Освободите его или замените левую часть проброса, например на 3001:3000. После этого открывайте http://localhost:3001/; порт справа менять не нужно.
«the following variables are required: POSTGRES_PASSWORD»
Переменная не задана в текущем терминале. Выполните команду для своей оболочки из шага 4 и повторите запуск. Переменная действует только в открытой сессии терминала.
«password authentication failed for user app_user»
Скорее всего, уже существует volume с базой, созданный с другим паролем. Если данные не нужны, остановите проект и удалите volume командой docker compose down -v, затем запустите его снова. Не используйте эту команду в проекте с важными данными: она удалит хранилище PostgreSQL.
«ECONNREFUSED» или контейнер app постоянно перезапускается
Проверьте docker compose logs db и docker compose logs app. Внутри Compose имя хоста должно быть db, порт — 5432; адрес localhost:5432 в приложении ошибочен.
Страница возвращает 502 или не открывается
Убедитесь, что приложение слушает 0.0.0.0, а не только 127.0.0.1. Также проверьте проброс 3000:3000 и состояние контейнеров через docker compose ps.
Что проверить перед переносом дальше
Этот Compose-файл подходит для локальной разработки, но не является готовой конфигурацией продакшена. Не храните реальные пароли в репозитории, замените простую переменную окружения на секретное хранилище, добавьте резервное копирование PostgreSQL и отдельный reverse proxy с TLS.
Следующим шагом подключите миграции схемы вместо создания таблицы при старте приложения. Затем измерьте время ответа /health и проверьте поведение после перезапуска базы. Отдельная тема — инвалидация кешей и миграции без простоя; здесь важен только запуск двух сервисов и их базовая проверка.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: Docker Compose documentation, Docker Compose startup order and healthchecks
Частые вопросы
Почему приложение не подключается к базе через localhost?
Нужно ли устанавливать Node.js на компьютер?
Что будет, если у меня другая версия Docker Compose?
Почему изменение server.js не появляется в контейнере?
Удалит ли docker compose down базу?
Можно ли использовать такой файл в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.