DevOps

Погружение в Docker: руководство для React и Vue разработчиков

BorisBoris
|
8 января 2026 г.
|
26 мин чтения
|
76 просмотров
Погружение в Docker: руководство для React и Vue разработчиков

Docker кардинально изменил подход к разработке и деплою веб-приложений. Если вы React или Vue разработчик и ещё не используете Docker — эта статья поможет вам начать. Мы разберём основные концепции, создадим рабочие конфигурации и рассмотрим реальные сценарии использования.

Зачем фронтенд-разработчику Docker?

Возможно, вы думаете: "Зачем мне Docker? Я же просто пишу JavaScript". Вот несколько причин:

  • "У меня работает" — больше не аргумент. Docker гарантирует одинаковое окружение на всех машинах
  • Быстрый onboarding — новый разработчик запускает проект одной командой вместо настройки Node.js, MongoDB, Redis и т.д.
  • Изолированные зависимости — разные проекты могут использовать разные версии Node.js без конфликтов
  • Продакшен-ready — то, что работает локально в Docker, будет работать и на сервере
  • Микросервисы — легко поднять связку из фронтенда, бэкенда, базы данных и других сервисов

Основные концепции Docker

Прежде чем писать код, разберём ключевые термины:

ТерминОписание
Image (образ)"Снимок" приложения со всеми зависимостями. Как ISO-образ диска
Container (контейнер)Запущенный экземпляр образа. Можно запустить несколько контейнеров из одного образа
DockerfileИнструкция для сборки образа. Описывает, какую ОС взять, что установить, какие файлы скопировать
docker-composeИнструмент для запуска нескольких связанных контейнеров (например, приложение + база данных)
VolumeПостоянное хранилище данных, которое сохраняется между перезапусками контейнера
RegistryХранилище образов (Docker Hub, GitLab Registry, GitHub Packages)

Как это работает?

Представьте, что Docker — это "виртуальный компьютер в компьютере", но гораздо легче и быстрее обычной виртуальной машины. Когда вы запускаете контейнер:

  1. Docker берёт образ (image) — это шаблон с операционной системой, Node.js, вашим кодом и всеми зависимостями
  2. Создаёт изолированный контейнер — как отдельный мини-компьютер со своей файловой системой и сетью
  3. Запускает ваше приложение внутри этого контейнера

Главное преимущество: контейнер всегда запускается одинаково, независимо от того, на какой машине он работает — на вашем MacBook, Windows-компьютере коллеги или Linux-сервере в облаке.

Установка Docker

Инструкция по установке Docker

macOS

Скачайте и установите Docker Desktop.

После установки проверьте:

docker --version
docker compose version

Windows

  1. Скачайте Docker Desktop
  2. Включите WSL 2 (Windows Subsystem for Linux)
  3. Установите Docker Desktop

Linux (Ubuntu/Debian)

# Установка Docker
curl -fsSL https://get.docker.com | sh

# Добавление пользователя в группу docker (чтобы не писать sudo)
sudo usermod -aG docker $USER

# Перезайдите в систему и проверьте
docker --version

Часть 1: Docker для Vue.js

Анатомия Dockerfile

Прежде чем писать Dockerfile, разберём основные инструкции:

ИнструкцияЧто делаетПример
FROMБазовый образ, на основе которого строим свойFROM node:20-alpine
WORKDIRУстанавливает рабочую директорию внутри контейнераWORKDIR /app
COPYКопирует файлы с вашего компьютера в контейнерCOPY package.json ./
RUNВыполняет команду при сборке образаRUN npm install
EXPOSEДокументирует, какой порт использует приложениеEXPOSE 3000
CMDКоманда, которая выполнится при запуске контейнераCMD ["npm", "start"]
ENVУстанавливает переменную окруженияENV NODE_ENV=production
ARGАргумент, передаваемый при сборкеARG API_URL

Простой Dockerfile для Vue 3 + Vite

Начнём с базового примера для проекта на Vue 3 с Vite:

Dockerfile для Vue 3 + Vite (development)
# FROM — указываем базовый образ
# node:20-alpine — это Node.js версии 20 на базе Alpine Linux
# Alpine — минималистичный дистрибутив Linux (~5MB вместо ~900MB у обычного)
FROM node:20-alpine

# WORKDIR — создаём и переходим в директорию /app внутри контейнера
# Все последующие команды будут выполняться в этой папке
WORKDIR /app

# COPY — копируем package.json и package-lock.json в контейнер
# Почему отдельно? Для оптимизации кэширования слоёв Docker
# Если код изменился, но зависимости нет — npm install не будет перезапускаться
COPY package*.json ./

# RUN — выполняем команду при СБОРКЕ образа
# npm ci — "чистая" установка зависимостей строго по package-lock.json
# Отличие от npm install: не изменяет lock-файл, быстрее, надёжнее для CI/CD
RUN npm ci

# Копируем весь остальной код проекта
COPY . .

# EXPOSE — документируем, что приложение слушает порт 5173
# Это НЕ открывает порт автоматически, только информирует пользователя
EXPOSE 5173

# CMD — команда, которая выполнится при ЗАПУСКЕ контейнера
# --host 0.0.0.0 — важно! По умолчанию Vite слушает только localhost
# Внутри контейнера localhost недоступен извне, поэтому привязываем к 0.0.0.0
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

Что здесь происходит пошагово:

  1. Берём готовый образ с Node.js 20 на базе лёгкого Alpine Linux
  2. Создаём папку /app — это будет "домашняя" директория нашего приложения
  3. Копируем файлы зависимостей отдельно (для оптимизации кэша Docker)
  4. Устанавливаем npm-пакеты
  5. Копируем весь исходный код
  6. Указываем порт и команду запуска

Запуск:

# Сборка образа (точка в конце — путь к Dockerfile)
# -t my-vue-app — даём образу имя (tag)
docker build -t my-vue-app .

# Запуск контейнера
# -p 5173:5173 — пробрасываем порт: [порт на хосте]:[порт в контейнере]
docker run -p 5173:5173 my-vue-app

После запуска откройте http://localhost:5173 — вы увидите ваше Vue-приложение.

Production Dockerfile с multi-stage сборкой

Для продакшена нам не нужны исходники и dev-зависимости — только собранные статические файлы. Здесь помогает multi-stage сборка.

Что такое multi-stage сборка?

Это техника, когда в одном Dockerfile описываются несколько этапов (stages). Каждый этап начинается с FROM и может использовать файлы из предыдущих этапов. В финальный образ попадает только последний этап.

Зачем это нужно?

  • Образ для сборки: Node.js, npm, TypeScript, все dev-зависимости — может весить 1GB+
  • Финальный образ: только nginx и статические файлы — ~50MB
Dockerfile для Vue 3 (production)
# =============================================
# Stage 1: Build (этап сборки)
# Этот этап существует только для сборки приложения
# Его содержимое НЕ попадёт в финальный образ
# =============================================
FROM node:20-alpine AS builder
# AS builder — даём имя этому этапу, чтобы ссылаться на него позже

WORKDIR /app

# Копируем файлы зависимостей
COPY package*.json ./

# Устанавливаем ВСЕ зависимости (включая devDependencies)
# Они нужны для сборки (Vite, TypeScript, и т.д.)
RUN npm ci

# Копируем исходный код
COPY . .

# Собираем приложение
# После этой команды появится папка dist/ с готовыми статическими файлами
RUN npm run build

# =============================================
# Stage 2: Production (финальный этап)
# Только этот этап попадёт в итоговый образ
# =============================================
FROM nginx:alpine AS production
# nginx:alpine — легковесный веб-сервер (~20MB)
# Он будет раздавать наши статические файлы

# COPY --from=builder — копируем файлы из этапа "builder"
# Берём только папку dist и кладём в директорию nginx для статики
COPY --from=builder /app/dist /usr/share/nginx/html

# Копируем нашу конфигурацию nginx (для SPA-роутинга)
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Nginx по умолчанию слушает порт 80
EXPOSE 80

# Запускаем nginx в foreground-режиме
# daemon off — важно для Docker, иначе контейнер сразу завершится
CMD ["nginx", "-g", "daemon off;"]

Результат:

  • Этап builder: ~1GB (Node.js + node_modules + исходники)
  • Финальный образ: ~50MB (nginx + HTML/CSS/JS файлы)

Разница в 20 раз! Это критично для:

  • Скорости деплоя (быстрее скачивать образ)
  • Размера хранилища в registry
  • Времени запуска контейнера
nginx.conf для Vue SPA
server {
    listen 80;
    server_name localhost;

    # Указываем корневую директорию со статикой
    root /usr/share/nginx/html;
    index index.html;

    # ===== Gzip сжатие =====
    # Сжимаем ответы для уменьшения трафика
    gzip on;
    gzip_vary on;           # Добавляет заголовок Vary: Accept-Encoding
    gzip_min_length 1024;   # Сжимаем файлы больше 1KB
    gzip_types text/plain text/css text/xml text/javascript
               application/javascript application/json application/xml;

    # ===== Кэширование статики =====
    # Файлы с хэшем в имени (main.a1b2c3.js) можно кэшировать надолго
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;                                    # Кэш на 1 год
        add_header Cache-Control "public, immutable";  # immutable = файл никогда не изменится
    }

    # ===== SPA Fallback =====
    # Это КРИТИЧЕСКИ важно для Single Page Application!
    #
    # Проблема: пользователь переходит на /about, нажимает F5
    # Браузер запрашивает /about у сервера
    # Но файла /about не существует — есть только index.html
    #
    # Решение: try_files пробует найти файл, если не находит — отдаёт index.html
    # Vue Router внутри index.html сам разберётся с роутом /about
    location / {
        try_files $uri $uri/ /index.html;
    }

    # ===== Проксирование API =====
    # Если бэкенд запущен в отдельном контейнере
    location /api/ {
        # backend — имя сервиса в docker-compose
        proxy_pass http://backend:3000/;
        proxy_http_version 1.1;

        # Передаём оригинальные заголовки
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Почему нужен кастомный nginx.conf?

Стандартный nginx не знает про SPA-роутинг. Без конфига:

  1. Пользователь на странице /users/123
  2. Нажимает F5 (обновить)
  3. Браузер запрашивает /users/123 у сервера
  4. Nginx: "Файла /users/123 нет" → 404 ошибка

С конфигом try_files $uri $uri/ /index.html:

  1. Nginx пробует найти файл /users/123 — не находит
  2. Пробует найти директорию /users/123/ — не находит
  3. Отдаёт /index.html
  4. Vue Router видит URL /users/123 и рендерит нужный компонент

Docker Compose: связываем несколько сервисов

Реальные приложения состоят из нескольких компонентов: фронтенд, бэкенд, база данных, кэш. Docker Compose позволяет описать всю инфраструктуру в одном файле и запустить одной командой.

Dockerfile для Nuxt 3 (SSR)
# =============================================
# Stage 1: Dependencies
# Устанавливаем только production-зависимости
# =============================================
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./

# --omit=dev — пропускаем devDependencies
# Эти зависимости будут скопированы в production-образ
RUN npm ci --omit=dev

# =============================================
# Stage 2: Build
# Собираем приложение со ВСЕМИ зависимостями
# =============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./

# Здесь устанавливаем ВСЕ зависимости — нужны для сборки
RUN npm ci

COPY . .

# Nuxt создаёт папку .output с сервером и статикой
RUN npm run build

# =============================================
# Stage 3: Production
# Финальный образ для запуска
# =============================================
FROM node:20-alpine AS runner
WORKDIR /app

# Создаём непривилегированного пользователя для безопасности
# Если приложение взломают — злоумышленник получит минимум прав
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nuxtjs

# Копируем ТОЛЬКО собранное приложение
# Исходники, node_modules с dev-зависимостями — не нужны
COPY --from=builder /app/.output ./.output

# Переключаемся на непривилегированного пользователя
USER nuxtjs

EXPOSE 3000

# Переменные окружения для Nuxt
ENV HOST=0.0.0.0
ENV PORT=3000
ENV NODE_ENV=production

# Запускаем Nuxt-сервер
CMD ["node", ".output/server/index.mjs"]
docker-compose.yml для Nuxt + MongoDB
# Версия формата docker-compose (можно не указывать для новых версий)
services:
  # ===== Nuxt приложение =====
  app:
    # build: . — собрать образ из Dockerfile в текущей директории
    build: .

    # Имя контейнера (для удобства в логах и командах)
    container_name: nuxt-app

    # Политика перезапуска:
    # - unless-stopped: перезапускать при падении, кроме ручной остановки
    # - always: перезапускать всегда
    # - on-failure: только при ненулевом exit code
    restart: unless-stopped

    # Проброс портов: [хост]:[контейнер]
    # localhost:3000 на хосте → порт 3000 в контейнере
    ports:
      - "3000:3000"

    # Переменные окружения для приложения
    environment:
      # mongo — это имя сервиса ниже, Docker автоматически
      # резолвит его в IP-адрес контейнера MongoDB
      - MONGODB_URI=mongodb://mongo:27017/mydb
      - NODE_ENV=production

    # Зависимости: app запустится только когда mongo будет "healthy"
    depends_on:
      mongo:
        condition: service_healthy  # Ждём прохождения healthcheck

    # Подключаем к внутренней сети
    networks:
      - app-network

  # ===== MongoDB =====
  mongo:
    # Используем готовый образ (не собираем свой)
    image: mongo:7

    container_name: mongodb
    restart: unless-stopped

    # Volumes — постоянное хранилище
    # Без volume данные пропадут при пересоздании контейнера!
    volumes:
      # mongo_data — именованный volume (объявлен внизу)
      # /data/db — директория MongoDB для данных
      - mongo_data:/data/db

    # Healthcheck — проверка здоровья сервиса
    # Docker будет периодически выполнять эту команду
    healthcheck:
      # Команда для проверки: пингуем MongoDB
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s    # Проверять каждые 10 секунд
      timeout: 5s      # Таймаут проверки
      retries: 5       # Сколько неудач до статуса "unhealthy"

    networks:
      - app-network

# ===== Networks (сети) =====
# Изолированная сеть для взаимодействия контейнеров
networks:
  app-network:
    driver: bridge  # Стандартный драйвер для локальной сети

# ===== Volumes (тома) =====
# Именованные тома для постоянного хранения данных
volumes:
  mongo_data:  # Данные MongoDB сохранятся между перезапусками

Ключевые концепции docker-compose:

1. Сервисы (services)

Каждый сервис — это контейнер. В примере выше два сервиса: app и mongo.

2. Сети (networks)

Контейнеры в одной сети могут обращаться друг к другу по имени сервиса. Внутри контейнера app можно писать mongodb://mongo:27017 — Docker автоматически подставит IP-адрес контейнера mongo.

3. Тома (volumes)

Данные внутри контейнера по умолчанию эфемерны — при удалении контейнера они пропадут. Volumes решают эту проблему — это постоянное хранилище, которое живёт независимо от контейнеров.

4. depends_on с healthcheck

Просто depends_on: [mongo] не гарантирует, что MongoDB успела запуститься. Контейнер запустится, но MongoDB внутри может инициализироваться ещё 10 секунд. С condition: service_healthy приложение стартует только когда MongoDB ответит на ping.

Основные команды:

# Собрать образы и запустить все сервисы в фоне
docker compose up -d --build

# Посмотреть логи (Ctrl+C для выхода)
docker compose logs -f app

# Статус контейнеров
docker compose ps

# Остановить и удалить контейнеры (volumes сохранятся)
docker compose down

# Остановить и удалить ВСЁ включая volumes
docker compose down -v

Часть 2: Docker для React

Простой Dockerfile для React + Vite

Dockerfile для React + Vite (development)
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

EXPOSE 5173

# Для Vite нужен флаг --host для доступа извне контейнера
# Без него Vite слушает только 127.0.0.1, который недоступен с хоста
CMD ["npm", "run", "dev", "--", "--host"]

Production Dockerfile для React

Dockerfile для React (production с nginx)
# =============================================
# Stage 1: Build
# =============================================
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

# ARG — аргументы времени сборки
# В отличие от ENV, они доступны ТОЛЬКО при сборке образа
# Используем для передачи URL API, который "вшивается" в бандл
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL

# После сборки переменные Vite будут "захардкожены" в JS-файлы
RUN npm run build

# =============================================
# Stage 2: Production
# =============================================
FROM nginx:alpine

COPY --from=builder /app/dist /usr/share/nginx/html

COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Важно про переменные окружения в Vite/React:

Vite (и Create React App) подставляют import.meta.env.VITE_* переменные при сборке, а не при запуске. Это значит:

// В вашем коде:
const apiUrl = import.meta.env.VITE_API_URL;

// После сборки превратится в:
const apiUrl = "https://api.example.com";

Переменная "захардкожена" в бандле. Чтобы изменить URL — нужно пересобирать образ.

Как передать переменную при сборке:

docker build --build-arg VITE_API_URL=https://api.example.com -t my-app .

Или в docker-compose:

services:
  app:
    build:
      context: .
      args:
        - VITE_API_URL=https://api.example.com
nginx.conf для React SPA
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Gzip сжатие для уменьшения размера ответов
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml text/javascript;

    # Кэширование статических файлов
    # Vite добавляет хэш в имена файлов, поэтому можно кэшировать навсегда
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA fallback для React Router
    # Любой путь, который не файл — отдаём index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Docker Compose для полного стека

Реальное приложение обычно включает фронтенд, бэкенд, базу данных и часто кэш. Рассмотрим типичную конфигурацию:

docker-compose.yml для React + Node.js + PostgreSQL + Redis
services:
  # ===== React Frontend =====
  frontend:
    build:
      context: ./frontend        # Путь к папке с фронтендом
      dockerfile: Dockerfile
      args:
        # URL API для Vite — передаётся при сборке
        - VITE_API_URL=http://localhost:4000
    container_name: react-frontend
    ports:
      - "80:80"
    # Фронтенд зависит от бэкенда (опционально)
    depends_on:
      - backend
    networks:
      - app-network

  # ===== Node.js Backend =====
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: node-backend
    restart: unless-stopped
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
      # DATABASE_URL: db — имя сервиса PostgreSQL
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      # Секреты лучше передавать через .env файл или Docker secrets
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app-network

  # ===== PostgreSQL =====
  db:
    image: postgres:16-alpine
    container_name: postgres-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password  # В продакшене используйте секреты!
      - POSTGRES_DB=myapp
    volumes:
      # Данные PostgreSQL сохраняются в volume
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      # pg_isready — утилита для проверки готовности PostgreSQL
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # ===== Redis =====
  redis:
    image: redis:alpine
    container_name: redis-cache
    restart: unless-stopped
    volumes:
      - redis_data:/data
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  postgres_data:
  redis_data:

Структура проекта для такой конфигурации:

my-project/
├── docker-compose.yml
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   ├── nginx.conf
│   └── src/
└── backend/
    ├── Dockerfile
    ├── package.json
    └── src/
Dockerfile для Node.js бэкенда
FROM node:20-alpine

WORKDIR /app

# Устанавливаем только production-зависимости
COPY package*.json ./
RUN npm ci --omit=dev

# Копируем исходники
COPY . .

# Собираем TypeScript (если используется)
RUN npm run build

EXPOSE 4000

# Безопасность: запускаем от встроенного пользователя node
# Он уже существует в образе node:*
USER node

CMD ["node", "dist/index.js"]

Next.js с Docker

Next.js сложнее Vue SPA из-за серверного рендеринга. Но Next.js 13+ поддерживает standalone output, который значительно упрощает Docker-конфигурацию:

Dockerfile для Next.js 14+
# =============================================
# Stage 1: Dependencies
# =============================================
FROM node:20-alpine AS deps

# libc6-compat нужен для некоторых npm-пакетов в Alpine
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package*.json ./
RUN npm ci

# =============================================
# Stage 2: Build
# =============================================
FROM node:20-alpine AS builder
WORKDIR /app

# Копируем node_modules из предыдущего этапа
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Отключаем телеметрию Next.js
ENV NEXT_TELEMETRY_DISABLED=1

RUN npm run build

# =============================================
# Stage 3: Production
# =============================================
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Создаём пользователя
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# Копируем публичные файлы
COPY --from=builder /app/public ./public

# standalone output содержит:
# - минимальный сервер
# - только используемые node_modules
# - всё необходимое для работы
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# standalone создаёт server.js — минималистичный Next.js сервер
CMD ["node", "server.js"]

Важно! Для standalone output добавьте в next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};

module.exports = nextConfig;

Что даёт standalone:

  • Размер образа: ~100-150MB вместо 500MB+
  • Все зависимости трейсятся и копируются автоматически
  • Готовый server.js без необходимости устанавливать next в production

Часть 3: Практические сценарии

Hot Reload в Docker для разработки

При разработке хочется, чтобы изменения в коде сразу отражались в браузере. Для этого нужно:

  1. "Монтировать" локальную папку с кодом внутрь контейнера
  2. Настроить file watcher для обнаружения изменений
docker-compose.dev.yml для разработки с hot reload
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: dev-app
    ports:
      - "5173:5173"
    volumes:
      # Монтируем текущую директорию внутрь /app в контейнере
      # Теперь любое изменение файла на хосте видно в контейнере
      - .:/app

      # НО! node_modules должны быть из контейнера, не с хоста
      # Причины:
      # 1. Нативные модули могут отличаться (bcrypt на Mac vs Linux)
      # 2. Избегаем конфликтов версий
      # Этот "анонимный volume" перекрывает папку node_modules
      - /app/node_modules

    environment:
      # Chokidar — библиотека для отслеживания изменений файлов
      # USEPOLLING=true нужен для Docker на Mac/Windows
      # Потому что fs.watch не работает корректно через mount
      - CHOKIDAR_USEPOLLING=true

    # Нужны для корректной работы терминала
    stdin_open: true
    tty: true
Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

# Копируем и устанавливаем зависимости
COPY package*.json ./
RUN npm install  # В dev используем install, не ci

# Исходники будут смонтированы через volume, копировать не нужно!
# При запуске папка /app будет содержать файлы с хоста

EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host"]

Как это работает:

  1. - .:/app — папка проекта на хосте "накладывается" на /app в контейнере
  2. Изменили файл src/App.vue на хосте → файл /app/src/App.vue в контейнере тоже изменился
  3. Vite видит изменение и обновляет браузер (HMR)

Запуск:

docker compose -f docker-compose.dev.yml up

Оптимизация размера образа

Сравните размеры образов:

ОбразРазмерКомментарий
node:20~1GBПолный Debian с Node.js
node:20-slim~200MBDebian без лишних пакетов
node:20-alpine~130MBМинимальный Alpine Linux
nginx:alpine~40MBТолько веб-сервер
Ваш SPA~50-70MBnginx + статика

Советы по оптимизации:

Лучшие практики для минимального размера
# 1. Используйте alpine образы
# Alpine — минималистичный Linux дистрибутив
FROM node:20-alpine

# 2. Устанавливайте только production зависимости в финальном образе
RUN npm ci --omit=dev

# 3. Используйте multi-stage builds
# Этап сборки с dev-зависимостями не попадает в финальный образ
FROM node:20-alpine AS builder
# ... сборка ...
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

# 4. Используйте .dockerignore
# Создайте файл .dockerignore в корне проекта:

.dockerignore:

node_modules
.git
.gitignore
README.md
Dockerfile*
docker-compose*
.env*
coverage
.nyc_output
dist
.next
.nuxt
# 5. Объединяйте RUN команды и очищайте кэш
# Каждая RUN создаёт новый слой, объединение уменьшает количество слоёв
RUN npm ci --omit=dev && \
    npm cache clean --force && \
    rm -rf /tmp/*

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

Есть несколько способов передать переменные в контейнер:

Работа с переменными окружения в Docker

1. Напрямую в docker-compose.yml:

services:
  app:
    environment:
      - API_URL=https://api.example.com
      - DEBUG=false

2. Через .env файл:

.env в той же папке, что и docker-compose.yml:

API_URL=https://api.example.com
DEBUG=false
services:
  app:
    env_file:
      - .env

3. Build-time vs Runtime переменные:

# ARG — доступен ТОЛЬКО при сборке (docker build)
ARG VITE_API_URL

# ENV — доступен при запуске контейнера (docker run)
ENV VITE_API_URL=$VITE_API_URL
services:
  app:
    build:
      args:
        - VITE_API_URL=https://api.example.com  # Build-time
    environment:
      - NODE_ENV=production                      # Runtime

4. Проблема с Vite/React:

Vite подставляет import.meta.env.VITE_* при сборке. Если нужны разные URL для разных окружений без пересборки — используйте runtime-конфигурацию:

// public/config.js — этот файл не проходит через Vite
window.__CONFIG__ = {
  API_URL: '%%API_URL%%'  // Плейсхолдер
};
<!-- index.html -->
<script src="/config.js"></script>
// Использование в приложении
const apiUrl = window.__CONFIG__.API_URL;
# entrypoint.sh — замена плейсхолдеров при старте контейнера
#!/bin/sh
# Заменяем плейсхолдер на значение переменной окружения
sed -i "s|%%API_URL%%|$API_URL|g" /usr/share/nginx/html/config.js
# Запускаем nginx
nginx -g 'daemon off;'
# В Dockerfile
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

Теперь можно менять API_URL без пересборки:

docker run -e API_URL=https://api.prod.com my-app

Healthcheck: проверка здоровья сервисов

Healthcheck позволяет Docker понять, работает ли приложение внутри контейнера корректно. Это критично для:

  • Оркестраторов (Kubernetes, Docker Swarm)
  • depends_on с condition: service_healthy
  • Автоматического перезапуска "зависших" контейнеров
Настройка healthcheck
services:
  frontend:
    build: .
    healthcheck:
      # Команда проверки: wget пытается получить главную страницу
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:80"]
      interval: 30s      # Как часто проверять
      timeout: 10s       # Сколько ждать ответа
      retries: 3         # Сколько неудач до "unhealthy"
      start_period: 10s  # Время на запуск приложения (не считается)

  backend:
    build: ./backend
    healthcheck:
      # curl проверяет эндпоинт /health
      # -f — fail при HTTP ошибках (4xx, 5xx)
      test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Эндпоинт /health в Node.js:

app.get('/health', (req, res) => {
  // Можно добавить проверку БД, Redis и т.д.
  res.status(200).json({ status: 'ok' });
});

Часть 4: CI/CD интеграция

Автоматическая сборка и деплой Docker-образов — стандартная практика. Рассмотрим конфигурации для популярных CI-систем.

GitHub Actions для Docker

.github/workflows/docker.yml
name: Build and Push Docker Image

# Когда запускать workflow
on:
  push:
    branches: [main]        # При push в main
  pull_request:
    branches: [main]        # При PR в main

# Переменные окружения для всего workflow
env:
  REGISTRY: ghcr.io                          # GitHub Container Registry
  IMAGE_NAME: ${{ github.repository }}       # username/repo-name

jobs:
  build:
    runs-on: ubuntu-latest

    # Права для работы с registry
    permissions:
      contents: read
      packages: write

    steps:
      # 1. Клонируем репозиторий
      - name: Checkout repository
        uses: actions/checkout@v4

      # 2. Логинимся в GitHub Container Registry
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}  # Автоматический токен

      # 3. Генерируем теги и метаданные для образа
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha                              # ghcr.io/user/repo:abc1234
            type=ref,event=branch                 # ghcr.io/user/repo:main
            type=raw,value=latest,enable={{is_default_branch}}

      # 4. Собираем и пушим образ
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          # Пушим только при push, не при PR
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # Кэширование слоёв для ускорения сборки
          cache-from: type=gha
          cache-to: type=gha,mode=max

GitLab CI для Docker

.gitlab-ci.yml
# Этапы pipeline
stages:
  - build
  - deploy

# Переменные
variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA    # registry.gitlab.com/user/repo:abc1234
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest

# ===== Сборка образа =====
build:
  stage: build
  # Docker-in-Docker: запускаем Docker внутри Docker
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    # Логинимся в GitLab Container Registry
    # CI_REGISTRY_* — встроенные переменные GitLab
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $IMAGE_TAG -t $IMAGE_LATEST .
    - docker push $IMAGE_TAG
    - docker push $IMAGE_LATEST
  only:
    - main  # Только для ветки main

# ===== Деплой на сервер =====
deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    # Устанавливаем SSH-клиент
    - apk add --no-cache openssh-client
    # Настраиваем SSH-ключ из переменной GitLab
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keyscan -H $VPS_HOST >> ~/.ssh/known_hosts
  script:
    # Подключаемся к серверу и обновляем контейнеры
    - ssh $VPS_USER@$VPS_HOST "cd /opt/app && docker compose pull && docker compose up -d"
  only:
    - main
  when: manual  # Ручной запуск (кнопка в GitLab UI)

Полезные команды Docker

Шпаргалка по командам
# ===== Образы =====
docker build -t myapp .              # Собрать образ с тегом myapp
docker build -t myapp:v1.0 .         # С версией
docker images                        # Список образов
docker rmi myapp                     # Удалить образ
docker image prune                   # Удалить неиспользуемые образы
docker tag myapp registry/myapp      # Переименовать/добавить тег

# ===== Контейнеры =====
docker run myapp                     # Запустить (блокирует терминал)
docker run -d myapp                  # Запустить в фоне (detached)
docker run -p 3000:3000 myapp        # С пробросом портов
docker run -e NODE_ENV=prod myapp    # С переменной окружения
docker run -v $(pwd):/app myapp      # С монтированием тома
docker run --name my-container myapp # С именем контейнера

docker ps                            # Запущенные контейнеры
docker ps -a                         # Все контейнеры (включая остановленные)
docker stop <id|name>                # Остановить контейнер
docker start <id|name>               # Запустить остановленный
docker rm <id|name>                  # Удалить контейнер
docker rm -f <id|name>               # Удалить даже запущенный

docker logs <id|name>                # Логи контейнера
docker logs -f <id|name>             # Логи в реальном времени (follow)
docker logs --tail 100 <id|name>     # Последние 100 строк

docker exec -it <id|name> sh         # Зайти в контейнер (shell)
docker exec <id|name> ls /app        # Выполнить команду в контейнере

# ===== Docker Compose =====
docker compose up                    # Запустить все сервисы
docker compose up -d                 # В фоне
docker compose up --build            # Пересобрать образы и запустить
docker compose up app                # Запустить только сервис app
docker compose down                  # Остановить и удалить контейнеры
docker compose down -v               # + удалить volumes

docker compose logs                  # Логи всех сервисов
docker compose logs -f app           # Логи сервиса app в реальном времени
docker compose ps                    # Статус сервисов
docker compose exec app sh           # Зайти в контейнер сервиса
docker compose restart app           # Перезапустить сервис

# ===== Очистка =====
docker system df                     # Использование диска
docker system prune                  # Удалить всё неиспользуемое
docker system prune -a               # + все образы без контейнеров
docker volume prune                  # Удалить неиспользуемые volumes
docker network prune                 # Удалить неиспользуемые сети

# ===== Отладка =====
docker inspect <id|name>             # Детальная информация о контейнере
docker stats                         # Использование ресурсов (CPU, RAM)
docker top <id|name>                 # Процессы в контейнере

Типичные ошибки и их решения

EACCES: permission denied

Проблема: Приложение не может записать файлы в директорию.

Причина: Контейнер запускается от root, а приложение от другого пользователя, или наоборот.

Решение:

# Создайте пользователя и дайте ему права на нужные директории
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
npm install зависает или падает

Проблема: Сборка образа останавливается на npm install.

Возможные причины и решения:

# 1. Не хватает памяти — увеличьте лимит для Node
ENV NODE_OPTIONS="--max-old-space-size=4096"

# 2. Проблемы с сетью — используйте npm ci (быстрее и надёжнее)
RUN npm ci

# 3. Много зависимостей — очищайте кэш
RUN npm ci && npm cache clean --force

В Docker Desktop: Settings → Resources → увеличьте Memory.

Hot reload не работает

Проблема: Изменения в коде не отражаются в браузере.

Причина: File watcher не видит изменения в смонтированном volume (особенно на Mac/Windows).

Решение:

# docker-compose.yml
environment:
  # Для Vite (использует Chokidar)
  - CHOKIDAR_USEPOLLING=true

  # Для Webpack
  - WATCHPACK_POLLING=true

  # Для Create React App (старый)
  - FAST_REFRESH=false
Контейнер сразу останавливается

Проблема: Контейнер запускается и сразу завершается.

Причина: Главный процесс завершился или упал.

Диагностика:

# Посмотрите логи
docker logs <container_id>

# Посмотрите exit code
docker inspect <container_id> --format='{{.State.ExitCode}}'

# Запустите интерактивно для отладки
docker run -it myapp sh

Частые причины:

  • Ошибка в CMD/ENTRYPOINT
  • Приложение упало при старте
  • Для nginx: забыли daemon off;
Cannot connect to the Docker daemon

Проблема: Cannot connect to the Docker daemon at unix:///var/run/docker.sock

Решения:

# 1. Docker Desktop не запущен — запустите его

# 2. На Linux: пользователь не в группе docker
sudo usermod -aG docker $USER
# Перезайдите в систему

# 3. Сервис Docker не запущен
sudo systemctl start docker
Port already in use

Проблема: Bind for 0.0.0.0:3000 failed: port is already allocated

Решение:

# Найдите, что занимает порт
lsof -i :3000  # Mac/Linux
netstat -ano | findstr :3000  # Windows

# Или используйте другой порт
docker run -p 3001:3000 myapp

Заключение

Docker — это мощный инструмент, который упрощает разработку, тестирование и деплой приложений. Для фронтенд-разработчика основные преимущества:

  1. Консистентность — одинаковое окружение везде
  2. Изоляция — никаких конфликтов версий
  3. Простой деплой — "собрал образ — запустил контейнер"
  4. Масштабирование — легко добавить инстансы

Рекомендуемый путь освоения:

  1. Начните с простого Dockerfile для вашего проекта
  2. Добавьте docker-compose для локальной разработки с базой данных
  3. Настройте multi-stage сборку для production
  4. Интегрируйте Docker в CI/CD пайплайн

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


Полезные ресурсы

Комментарии

Войдите, чтобы оставить комментарий

Пока нет комментариев. Будьте первым!