Погружение в 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 — это "виртуальный компьютер в компьютере", но гораздо легче и быстрее обычной виртуальной машины. Когда вы запускаете контейнер:
- Docker берёт образ (image) — это шаблон с операционной системой, Node.js, вашим кодом и всеми зависимостями
- Создаёт изолированный контейнер — как отдельный мини-компьютер со своей файловой системой и сетью
- Запускает ваше приложение внутри этого контейнера
Главное преимущество: контейнер всегда запускается одинаково, независимо от того, на какой машине он работает — на вашем MacBook, Windows-компьютере коллеги или Linux-сервере в облаке.
Установка Docker
Инструкция по установке Docker
macOS
Скачайте и установите Docker Desktop.
После установки проверьте:
docker --version
docker compose version
Windows
- Скачайте Docker Desktop
- Включите WSL 2 (Windows Subsystem for Linux)
- Установите 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"]
Что здесь происходит пошагово:
- Берём готовый образ с Node.js 20 на базе лёгкого Alpine Linux
- Создаём папку
/app— это будет "домашняя" директория нашего приложения - Копируем файлы зависимостей отдельно (для оптимизации кэша Docker)
- Устанавливаем npm-пакеты
- Копируем весь исходный код
- Указываем порт и команду запуска
Запуск:
# Сборка образа (точка в конце — путь к 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-роутинг. Без конфига:
- Пользователь на странице
/users/123 - Нажимает F5 (обновить)
- Браузер запрашивает
/users/123у сервера - Nginx: "Файла
/users/123нет" → 404 ошибка
С конфигом try_files $uri $uri/ /index.html:
- Nginx пробует найти файл
/users/123— не находит - Пробует найти директорию
/users/123/— не находит - Отдаёт
/index.html - 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 для разработки
При разработке хочется, чтобы изменения в коде сразу отражались в браузере. Для этого нужно:
- "Монтировать" локальную папку с кодом внутрь контейнера
- Настроить 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"]
Как это работает:
- .:/app— папка проекта на хосте "накладывается" на/appв контейнере- Изменили файл
src/App.vueна хосте → файл/app/src/App.vueв контейнере тоже изменился - Vite видит изменение и обновляет браузер (HMR)
Запуск:
docker compose -f docker-compose.dev.yml up
Оптимизация размера образа
Сравните размеры образов:
| Образ | Размер | Комментарий |
|---|---|---|
node:20 | ~1GB | Полный Debian с Node.js |
node:20-slim | ~200MB | Debian без лишних пакетов |
node:20-alpine | ~130MB | Минимальный Alpine Linux |
nginx:alpine | ~40MB | Только веб-сервер |
| Ваш SPA | ~50-70MB | nginx + статика |
Советы по оптимизации:
Лучшие практики для минимального размера
# 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 — это мощный инструмент, который упрощает разработку, тестирование и деплой приложений. Для фронтенд-разработчика основные преимущества:
- Консистентность — одинаковое окружение везде
- Изоляция — никаких конфликтов версий
- Простой деплой — "собрал образ — запустил контейнер"
- Масштабирование — легко добавить инстансы
Рекомендуемый путь освоения:
- Начните с простого Dockerfile для вашего проекта
- Добавьте docker-compose для локальной разработки с базой данных
- Настройте multi-stage сборку для production
- Интегрируйте Docker в CI/CD пайплайн
Не пытайтесь сразу использовать все возможности — начните с малого и постепенно добавляйте сложность по мере необходимости.
Полезные ресурсы
- Официальная документация Docker — исчерпывающее руководство
- Docker Hub — репозиторий готовых образов
- Play with Docker — бесплатная песочница для экспериментов
- Dockerfile best practices — официальные рекомендации
- Awesome Docker — подборка ресурсов на GitHub
Комментарии
Войдите, чтобы оставить комментарий
Пока нет комментариев. Будьте первым!