Контейнеризация Node.js и PostgreSQL с Docker

Процесс развёртывания и запуска приложений в разных средах может быть сложным: нужно учесть переменные окружения, зависимости, версии пакетов и параметры базы данных. Docker решает большинство этих проблем — контейнер содержит код и все его зависимости, поэтому приложение запускается одинаково в локальной, тестовой и продуктивной средах.
Что такое Docker?
Docker — платформа для разработки и доставки приложений, которая упаковывает приложение и все его зависимости в переносимый образ (image). Образ запускается как контейнер — изолированный процесс с собственным файловым пространством и сетью.
Кратко: образ — неизменяемый артефакт, контейнер — запущенный экземпляр образа.
Прежде чем начать, установите Docker Desktop или Docker Engine на вашу машину. Проверьте системные требования и инструкции по установке в официальной документации Docker для вашей ОС.
Пример: создаём Node.js REST API
В этом руководстве мы создаём простой Node.js сервер с тремя маршрутами и подключением к PostgreSQL через knex и pg. Исходный код можно хранить в репозитории GitHub.
Установите зависимости проекта:
npm install morgan pg knexПояснения по пакетам:
- pg — драйвер для подключения к PostgreSQL.
- knex — query builder для SQL (удобная обёртка для написания запросов).
- morgan — middleware для логирования HTTP-запросов в консоли.
Создайте файл index.js и добавьте следующий код сервера:
const express = require("express")
const morgan = require("morgan")
const app = express()
const db = require('./db')
const PORT = process.env.PORT || 5000
app.use(morgan('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => res.send('Hello World!'))
app.get('/users', async (req, res) => {
const users = await db.select().from('users')
res.json(users)
})
app.post('/users', async (req, res) => {
const user = await db('users').insert({ name: req.body.name }).returning('*')
res.json(user)
})
app.listen(PORT, () => console.log(`Server up at PORT:${PORT}`))Настройка подключения к базе данных
Создайте файл db.js в корне проекта со следующим содержимым. Он настраивает knex для соединения с PostgreSQL, где хост будет именем сервиса в docker-compose (db).
const knex = require('knex')
module.exports = knex({
client: 'postgres',
connection: {
host: 'db',
user: 'testUser',
password: 'mypassword123',
database: 'testUser'
}
})Важно: в контейнеризированной сети Docker Compose сервисы видят друг друга по имени сервиса (здесь — db). В локальной среде вы, возможно, будете использовать localhost или другой хост.
Скрипты миграции и сидирования
Создайте папку scripts и добавьте migrate.js и seed.js, чтобы автоматически создать таблицу users и добавить тестовые записи.
migrate.js:
const db = require('../db');
(async () => {
try {
await db.schema.dropTableIfExists('users')
await db.schema.withSchema('public').createTable('users', (table) => {
table.increments()
table.string('name')
})
console.log('Created users table!')
process.exit(0)
} catch (err) {
console.log(err)
process.exit(1)
}
})()seed.js:
const db = require('../db');
(async () => {
try {
await db('users').insert({ name: 'Test User1' })
await db('users').insert({ name: 'Test User2' })
console.log('Added dummy users!')
process.exit(0)
} catch (err) {
console.log(err)
process.exit(1)
}
})()Добавьте команды в package.json, чтобы запускать миграции и сиды через npm run:
"scripts": {
"start": "node index.js",
"migrate": "node scripts/migrate.js",
"seed": "node scripts/seed.js"
}Примечание: если структура папок иная, проверьте относительные пути require в скриптах.
Dockerfile для Node.js приложения
Создайте файл Dockerfile в корне проекта. Он определяет образ для приложения.
FROM node:16.3.0-alpine3.13
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD [ "node", "index.js" ]Разбор инструкций:
- FROM — базовый образ Node.js (alpine — компактная версия).
- WORKDIR — рабочая директория внутри контейнера.
- COPY package*.json ./ — копируем package.json и package-lock.json (если есть) для установки зависимостей до копирования всего кода (лучше для кэширования слоёв).
- RUN npm install — устанавливаем зависимости в образе.
- COPY . . — копируем весь код в контейнер.
- EXPOSE 8000 — декларативно указываем порт приложения внутри контейнера (важно: порт внутри контейнера должен соответствовать коду; в index.js мы используем порт 5000 по умолчанию, синхронизируйте значение).
- CMD — команда при запуске контейнера.
Важно: в примере EXPOSE указан 8000, а в приложении порт — 5000. Проверьте соответствие. В production лучше задать PORT через переменные окружения.
Docker Compose: связываем приложение и базу данных
Создайте docker-compose.yml в корне проекта. Он описывает сервисы: server и db.
version: '3.9'
services:
server:
build: .
ports:
- '5000:5000'
depends_on:
- db
environment:
- PORT=5000
- NODE_ENV=development
db:
image: 'postgres'
ports:
- '4321:5432'
environment:
POSTGRES_PASSWORD: 'mypassword123'
POSTGRES_USER: 'testUser'
POSTGRES_DB: 'testUser'
volumes:
- data:/var/lib/postgresql/data
volumes:
data:Ключевые моменты:
- ports: ‘5000:5000’ — прокидывает порт приложения наружу (хост:контейнер).
- depends_on: db — гарантирует порядок запуска контейнеров, но не гарантирует готовность сервиса. Следует ожидать готовности БД (см. раздел Troubleshooting).
- volumes: data — сохраняет данные PostgreSQL между перезапусками контейнеров.
Пример для разработки: bind-mount и файлы с окружением
Для локальной разработки удобно использовать bind-mount, чтобы код на хосте отражался в контейнере без пересборки образа:
server:
build: .
volumes:
- .:/app
- /app/node_modules
ports:
- '5000:5000'
environment:
- NODE_ENV=developmentДобавьте .env файл и подключите его через env_file: .env, если хотите хранить секреты вне repo (но не коммитьте .env в публичные репозитории).
Сборка и запуск контейнеров
Соберите и запустите сервисы в фоне:
docker-compose up -d --buildПроверьте логи сервиса:
docker-compose logs -f serverТестирование REST API и выполнение миграций
Выполните миграцию внутри контейнера сервера:
docker exec -it npm run migrate В оригинальном примере команда выглядела так:
docker exec docker_node-server-1 npm run migrateГде docker_node-server-1 — автоматически сгенерированное имя контейнера; используйте docker ps чтобы узнать имя или ID.
Ожидаемый результат — в логах появится сообщение о создании таблицы и в консоли вы увидите вывод скрипта.
Публикация образа на Docker Hub
- Зарегистрируйтесь в Docker Hub и создайте репозиторий (настройте Public или Private).
- Войдите в Docker через терминал:
docker login- Пометьте (tag) образ именем вашего репозитория:
docker tag /: - Отправьте образ в Docker Hub:
docker push /: После этого образ станет доступен для скачивания и использования на других хостах.
Лучшие практики и рекомендации
- Секреты и пароли храните в менеджере секретов или в переменных окружения CI — не в Dockerfile и не в репозитории.
- Используйте multistage сборку, чтобы уменьшить размер образа и убрать dev зависимости.
- Пиньте версии базовых образов и зависимостей (например, node:16.3.0-alpine3.13), чтобы сборки были воспроизводимы.
- Обновляйте зависимости и базовые образы по расписанию безопасности.
- В production используйте не root-пользователя внутри контейнера.
- Настройте healthcheck в docker-compose для автоматической проверки готовности сервисов.
Пример healthcheck для db:
db:
image: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testUser"]
interval: 10s
timeout: 5s
retries: 5Безопасность и жёсткая настройка
- Минимизируйте набор установленных пакетов в образе.
- Запускайте контейнеры с ограниченными правами (user, capabilities).
- Ограничивайте ресурсы контейнера (cpus, memory) в docker-compose для предотвращения DoS.
- Шифруйте трафик между сервисами (TLS) в продуктиве.
- Регулярно сканируйте образы на уязвимости (clair, trivy).
Отладка и типичные проблемы
- Проблема: сервер не может подключиться к БД сразу после запуска — причина: БД ещё не готова. Решение: добавьте healthcheck и механизм повторных попыток подключения в приложении.
- Проблема: несоответствие портов (EXPOSE 8000, но контейнер слушает 5000). Решение: синхронизируйте PORT (в коде и в Dockerfile/docker-compose).
- Проблема: данные не сохраняются после удаления контейнера. Решение: используйте volumes в docker-compose (named volumes или bind-mount на хост).
- Проблема: «permission denied» при записи в volume. Решение: проверьте владельца и права на папку на хосте или используйте user в контейнере.
CI/CD: подсказки для автоматической доставки
- В CI собирайте образ и пушьте в приватный реестр (Docker Hub, GitHub Container Registry, GitLab Registry).
- В пайплайне выполняйте тесты, миграции и проверку на уязвимости до деплоя.
- Для деплоя используйте target-окружения и стратегии (Blue/Green, Rolling) — зависит от платформы (Kubernetes, ECS, Docker Swarm).
Шаблон чеклистов по ролям
Разработчик:
- Локально протестировал API
- Обновил package.json и package-lock.json
- Обновил миграции/сиды
DevOps:
- Написал Dockerfile и docker-compose.yml
- Настроил volumes и резервное копирование БД
- Настроил CI для сборки и пуша образа
QA:
- Проверил основные API-эндпоинты
- Проверил поведение при падении БД и восстановлении
- Проверил миграции на чистой базе
Критерии приёмки
- Приложение запускается из docker-compose и отвечает на / и /users.
- Скрипты миграции и сидирования выполняются без ошибок.
- Данные сохраняются между перезапусками (volume работает).
- Образ опубликован в реестре и может быть скачан и запущен на другом хосте.
Маленькая шпаргалка команд
- Сборка и запуск: docker-compose up -d –build
- Остановка: docker-compose down
- Просмотр логов: docker-compose logs -f server
- Список контейнеров: docker ps
- Войти внутрь контейнера: docker exec -it
/bin/sh - Тег и пуш образа: docker tag / docker push
Решение: когда подходит, когда нет
Подходит:
- Быстрая упаковка приложения и его зависимостей.
- Локальная разработка, тестирование интеграций, развертывание в облаке.
Не подходит или требует осторожности:
- Задачи, где нужна тонкая виртуализация ядра или специфичные права хоста.
- Сценарии с высокими требованиями к производительности без оптимизации образов.
Заключение
Docker упрощает развёртывание приложений, сводя к минимум различия между средами. В этом руководстве показано, как контейнеризовать простое Node.js приложение с PostgreSQL, настроить миграции и сиды, протестировать и опубликовать образ. Также приведены рекомендации по безопасности, отладке и CI/CD.
Важное: всегда проверяйте соответствие портов и корректно храните секреты.
Краткое резюме:
- Упакуйте приложение и зависимости в Dockerfile.
- Опишите сервисы в docker-compose.yml и используйте volumes для сохранения данных.
- Добавьте healthchecks и retry-механизмы для надёжности.
Похожие материалы
Перенаправление вывода в bash: >, >> и tee
Опросы в WhatsApp: как создать и отслеживать
Звуки оповещений в Ubuntu — изменить и добавить
Купить набор Stand With Ukraine — Humble Bundle
Easy Diffusion: локальная генерация AI-искусства