Почему ваш JavaScript-проект заслуживает Docker?
Вы когда-нибудь слышали фразу: «На моей машине всё работало!»? Если вы JavaScript-разработчик, то наверняка сталкивались с ситуацией, когда приложение отлично запускается локально, но на сервере или у коллеги — падает с загадочной ошибкой. Контейнеризация JavaScript приложений с Docker решает эту проблему раз и навсегда.
Docker позволяет упаковать ваш код, все зависимости (npm-пакеты), переменные окружения и даже версию Node.js в один легковесный «контейнер». Этот контейнер будет работать одинаково на вашем ноутбуке, на сервере в AWS и на компьютере новичка, который только что установил Git. В этой статье мы разберём, как подружить JavaScript и Docker, напишем реальный код и создадим production-ready образ для вашего следующего pet-проекта.
Основы Docker для JavaScript-разработчика
Прежде чем нырять в код, давайте разберёмся с ключевыми понятиями. Docker — это как виртуальная машина, но гораздо легче. Вместо того чтобы эмулировать целую операционную систему, он использует ядро хоста и изолирует только ваше приложение.
Dockerfile — рецепт вашего приложения
Это текстовый файл, в котором вы описываете, как собрать образ. Для Node.js приложения типичный Dockerfile выглядит так:
# Используем официальный образ Node.js
FROM node:18-alpine
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем зависимости
RUN npm ci --only=production
# Копируем исходный код
COPY . .
# Открываем порт
EXPOSE 3000
# Команда для запуска
CMD ["node", "server.js"]
Обратите внимание: мы сначала копируем package.json, а потом весь код. Это не случайность. Docker кеширует слои, и если вы не меняли зависимости, этап npm ci не будет перезапускаться при каждом изменении кода. Это сильно ускоряет сборку.
Образы и контейнеры
- Образ (Image) — это шаблон. Вы собираете его один раз.
- Контейнер (Container) — это запущенный экземпляр образа. Вы можете создать 10 контейнеров из одного образа.
Создаём Node.js приложение с нуля и контейнеризируем его
Давайте напишем простое Express-приложение и упакуем его в Docker. Это будет наш первый практический шаг в контейнеризации JavaScript.
Шаг 1: Пишем приложение
Создайте папку docker-node-app и внутри — файл server.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Привет из контейнера!',
nodeVersion: process.version,
environment: process.env.NODE_ENV || 'development'
});
});
app.listen(PORT, () => {
console.log(`Сервер запущен на порту ${PORT}`);
});
Инициализируем npm и установим express:
npm init -y
npm install express
Теперь у нас есть рабочее приложение. Запустите его локально: node server.js. Откройте http://localhost:3000 — вы увидите JSON.
Шаг 2: Создаём Dockerfile
Создайте файл Dockerfile (без расширения) в корне проекта:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Добавим .dockerignore, чтобы не копировать лишние файлы в образ:
node_modules
npm-debug.log
.git
.gitignore
.env
Dockerfile
Шаг 3: Собираем и запускаем
Откройте терминал в папке проекта и выполните:
docker build -t my-node-app .
docker run -p 3000:3000 my-node-app
Флаг -p 3000:3000 пробрасывает порт: первый — это порт на вашей машине, второй — порт внутри контейнера. Теперь откройте http://localhost:3000 — ваше приложение работает в контейнере!
Продвинутые техники: многослойная сборка (Multi-stage build)
Когда вы работаете над крупным JavaScript-проектом, особенно с фронтендом, образ может «распухать». Для production-сборки нам не нужны dev-зависимости и инструменты сборки. Здесь на помощь приходит многослойная сборка.
Пример: React приложение в Docker
Представьте, что у нас есть React-приложение, собранное с помощью Vite. Мы хотим, чтобы финальный образ содержал только статические файлы, которые раздаёт Nginx.
# Этап 1: Сборка
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Этап 2: Продакшн
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Первый этап (builder) устанавливает все зависимости и собирает статику. Второй этап берёт только готовую папку dist и копирует её в чистый образ Nginx. Итоговый образ весит около 20 МБ вместо 500 МБ. Это лучшая практика контейнеризации фронтенда.
Docker Compose: оркеструем несколько сервисов
Редко какое приложение состоит только из Node.js. Часто есть база данных, Redis или очередь сообщений. Docker Compose позволяет описать всю инфраструктуру в одном YAML-файле и запускать одной командой.
Пример: Node.js + MongoDB
Создайте файл docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MONGO_URI=mongodb://mongo:27017/mydb
- NODE_ENV=production
depends_on:
- mongo
mongo:
image: mongo:6
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
Теперь в коде вашего приложения можно подключиться к MongoDB по адресу mongo — Docker Compose автоматически создаёт внутреннюю сеть, где сервисы видят друг друга по имени.
Запуск одной командой:
docker-compose up -d
Флаг -d запускает контейнеры в фоне. Чтобы остановить: docker-compose down. Это идеальный инструмент для локальной разработки и тестирования.