Docker для фронтенд- и бэк-разработчиков: практический гайд без DevOps-магии

Docker для фронтенд- и бэк-разработчиков: практический гайд без DevOps-магии

Docker для фронтенд- и бэк-разработчиков: практический гайд без DevOps-магии

Docker — это не «магия для DevOps», а нормальный инженерный инструмент, который решает очень приземлённую проблему:

один и тот же проект должен одинаково запускаться локально, у коллеги и на сервере.

Если ты фронтендер, бэкендер, PHP- или Node-разработчик — Docker нужен тебе ровно для этого.

Не для Kubernetes, не для резюме, не для пафоса.

Ниже — честный и практический гайд. Строго по документации, с примерами из реальной разработки.

Что такое контейнер и почему он лучше VM

Контейнер простыми словами

Контейнер — это изолированный процесс в Linux с:

  • собственным файловым пространством,
  • своей сетью,
  • своими зависимостями.

Контейнер не содержит своей операционной системы — только процесс(ы) и файлы из образа.

Контейнер vs VirtualBox / KVM — на одном примере

Задача: запустить Node.js + PostgreSQL для проекта.

Виртуальная машина (VirtualBox / KVM)

  1. Создаёшь VM с Ubuntu
  2. Ставишь Node.js
  3. Ставишь PostgreSQL
  4. Ловишь конфликт версий
  5. Настраиваешь systemd
  6. VM весит 2–4 ГБ
  7. Запускается минутами

Docker


docker compose up

  • Node и Postgres — в контейнерах
  • Вес — сотни мегабайт
  • Запуск — секунды
  • У всех разработчиков одинаковое окружение

Ключевая разница:

VM виртуализирует железо , Docker изолирует процессы.

Основы Docker

Установка Docker

Ubuntu


sudo apt update

sudo apt install -y docker.io docker-compose-plugin

sudo usermod -aG docker $USER

Перелогинься, иначе Docker будет требовать sudo.

CentOS / Rocky / AlmaLinux


sudo dnf install -y docker

sudo systemctl enable --now docker

Windows (через WSL2)

  1. Установить Docker Desktop
  2. Включить WSL2
  3. Docker работает в Linux , а не в Windows

Docker без WSL2 на Windows — плохая идея. Не надо так.

Основные понятия Docker

  • Image (образ) — шаблон (read-only)
  • Container (контейнер) — запущенный образ
  • Layer (слой) — шаг сборки образа
  • Registry — хранилище образов (Docker Hub, private registry)

Базовые команды Docker


docker run nginx

docker ps

docker ps -a

docker logs container_name

docker exec -it container_name sh

docker run vs docker exec

  • docker run — создаёт и запускает новый контейнер
  • docker exec — входит в уже запущенный контейнер

Типичная ошибка новичков — пытаться exec в контейнер, который не запущен.

Типичные ошибки новичков

  • Хранить данные внутри контейнера
  • Использовать тег latest
  • Запускать сервисы через service nginx start
  • Не читать docker logs
  • Копировать весь проект без .dockerignore

Dockerfile: собираем образ

Базовая структура Dockerfile


FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

EXPOSE 3000

CMD ["npm","run","start"]

Основные инструкции

  • FROM — базовый образ
  • RUN — команда при сборке
  • COPY — копирование файлов
  • ENV — переменные окружения
  • EXPOSE — документируем порт
  • CMD — команда запуска контейнера
  • ENTRYPOINT — точка входа

Пример 1: Dockerfile для Node.js (Astro / Express)


FROM node:20-alpine AS build

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

FROM node:20-alpine

WORKDIR /app

COPY --from=build /app/dist ./dist

COPY package*.json ./

RUN npm ci --omit=dev

EXPOSE 3000

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

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

  • multistage-сборка
  • dev-зависимости не попадают в production
  • минимальный размер образа

Пример 2: Dockerfile для PHP (WordPress / Bitrix)


FROM php:8.4-fpm-alpine

RUN apk add --no-cache 

bash git icu-dev libzip-dev oniguruma-dev 

&& docker-php-ext-install intl zip mysqli opcache

WORKDIR /var/www/html

WordPress и Bitrix официально используют php-fpm.

Apache внутри контейнера почти всегда лишний.

.dockerignore — обязательно

Минимальный набор (скопируй в корень проекта):


node_modules

vendor

.git

.gitignore

.env

.env.local

.env.*.local

*.log

npm-debug.log*

.DS_Store

coverage

.nyc_output

dist

.next

Расширенный вариант для Node.js (ещё меньше контекста — быстрее сборка):


node_modules

vendor

.git

.gitignore

.env*

*.log

.DS_Store

coverage

dist

.next

.nuxt

.cache

*.md

!README.md

Без .dockerignore:

  • образы раздуваются,
  • ломается кеш,
  • сборка становится медленной.

Рекомендации по Dockerfile

  • Используй alpine
  • Объединяй RUN в один слой
  • COPY package.json до копирования кода
  • Всегда используй multistage, если есть сборка

Проверка размера образа:


docker image ls

Пример вывода (образ без alpine и с лишними слоями будет в разы больше):


REPOSITORY TAG IMAGE ID CREATED SIZE

myapp latest a1b2c3d4e5f6 2 minutes ago 180MB

Сборка с тегом и без кеша (если что-то пошло не так):


docker build --no-cache -t myapp:1.0 .

Минимальный рабочий пример (copy-paste)

Ниже — полный набор файлов, чтобы поднять Node.js + PostgreSQL за минуту.

Dockerfile в корне проекта:


FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

docker-compose.yml :


services:

app:

build: .

ports:

- "3000:3000"

environment:

DATABASE_URL: postgresql://postgres:postgres@db:5432/app

depends_on:

db:

condition: service_healthy

db:

image: postgres:16-alpine

environment:

POSTGRES_USER: postgres

POSTGRES_PASSWORD: postgres

POSTGRES_DB: app

volumes:

- pgdata:/var/lib/postgresql/data

healthcheck:

test: ["CMD-SHELL", "pg_isready -U postgres"]

interval: 2s

timeout: 5s

retries: 5

volumes:

pgdata:

Запуск и проверка:


docker compose up -d

docker compose ps

curl -s http://localhost:3000

docker compose logs -f app

Docker Compose для разработки

docker-compose.yml (версия 3.x)


version: "3.9"

services:

app:

build: .

ports:

- "3000:3000"

env_file:

- .env

Типовые сервисы

  • nginx
  • php-fpm
  • mysql / postgres
  • redis

Один сервис — один контейнер. Всегда.

Networks и volumes

  • network — контейнеры общаются по имени сервиса
  • volume — постоянные данные
  • bind mount — файлы проекта (локальная разработка)

Пример 1: WordPress (nginx + php-fpm + mysql + phpMyAdmin)


services:

nginx:

image: nginx:alpine

volumes:

- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./wp:/var/www/html

ports:

- "8080:80"

php:

build: .

volumes:

- ./wp:/var/www/html

environment:

DB_HOST: db

depends_on:

- db

db:

image: mysql:8

environment:

MYSQL_ROOT_PASSWORD: root

MYSQL_DATABASE: wordpress

phpmyadmin:

image: phpmyadmin/phpmyadmin

environment:

PMA_HOST: db

ports:

- "8081:80"

Пример 2: Node.js + PostgreSQL + Redis


services:

app:

build: .

ports:

- "3000:3000"

depends_on:

- db
- redis

db:

image: postgres:16

volumes:

- pgdata:/var/lib/postgresql/data

redis:

image: redis:7

volumes:

pgdata:

Env-файлы

  • .env — локальная разработка
  • .env.production — продакшен
  • Секреты не коммить

Команды Docker Compose


docker compose up -d

docker compose down

docker compose ps

docker compose logs -f app

Как дебажить контейнер в Compose

Войти в оболочку запущенного сервиса:


docker compose exec app sh

Внутри контейнера можно проверить переменные, установленные пакеты и сеть:


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

env | grep DATABASE

Есть ли сеть до базы

nc -zv db 5432

Альпийский образ: вместо curl часто есть wget

wget -qO- http://localhost:3000

Выйти из контейнера: exit.

Логирование и отладка

docker logs


docker logs -f container_name

Если контейнер упал — причина почти всегда в логах.

docker inspect


docker inspect container_name

Там:

  • IP,
  • volumes,
  • env,
  • команды запуска.

Проброс портов


localhost:3000 → container:3000

Если порт не проброшен — с хоста до приложения в контейнере не достучаться.

localhost и IP-адреса внутри контейнера

Внутри Docker:

  • ❌ 127.0.0.1
  • ❌ localhost
  • ✅ имя сервиса (db, redis)

Docker DNS работает по имени сервиса.

Деплой контейнера на сервер

Production-сборка

  • без dev-зависимостей
  • без hot-reload
  • без bind-mount

Публикация образа в registry


docker build -t username/app:1.0 .

docker push username/app:1.0

Можно использовать Docker Hub или приватный registry.

Запуск на сервере через docker run

На сервере (после docker pull или если образ уже в registry):


docker run -d 

--name app 

-p 80:3000 

--restart=always 

-e NODE_ENV=production 

username/app:1.0

Проверка, что контейнер работает:


docker ps

curl -s -o /dev/null -w "%{http_code}" http://localhost:80

systemd unit-файл


[Unit]

Description=Docker App

After=docker.service

[Service]

Restart=always

ExecStart=/usr/bin/docker run --rm -p 80:3000 username/app:1.0

ExecStop=/usr/bin/docker stop app

[Install]

WantedBy=multi-user.target

Данные в production

  • volumes — базы данных
  • bind mounts — конфиги

Контейнеры можно удалять, данные — нет.

Обновление без простоя

  1. Поднять новый контейнер
  2. Переключить трафик (nginx)
  3. Остановить старый

Для небольших проектов этого достаточно.

Про Kubernetes — честно

Когда нужен Kubernetes

  • десятки сервисов
  • автоскейлинг
  • отказоустойчивость
  • несколько окружений

Базовые сущности K8s

  • Pod — один или несколько контейнеров
  • Deployment — управление версиями
  • Service — доступ к подам

Когда Kubernetes не нужен

  • один сервер
  • один проект
  • небольшая команда

В этом случае Docker Compose — лучше.

Альтернативы Kubernetes

  • Docker Swarm
  • AWS ECS
  • HashiCorp Nomad

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

Контейнер стартует и сразу падает

Причина — ошибка в CMD или ENTRYPOINT. Сначала смотри логи:


docker logs container_name

Если контейнер сразу падает и логи пустые — запусти образ без флага -d , чтобы увидеть вывод в консоли:


docker run --rm --name debug-app -p 3000:3000 myapp:latest

Ошибка (например, «Cannot find module») появится сразу в терминале. После исправления кода пересобери образ и снова запусти контейнер.

Port already in use

Узнай, какой процесс занял порт, и заверши его:


Linux / macOS

lsof -i :3000

или

sudo ss -tlnp | grep 3000

Убить процесс по PID (подставь реальный PID из вывода)

kill -9 PID

На Windows (WSL2) порт может держать другой контейнер — проверь docker ps и останови старый контейнер: docker stop container_name.

localhost внутри контейнера — это не хост

Для контейнера localhost и 127.0.0.1 указывают на сам контейнер , а не на твою машину. Это нормально: контейнер изолирован, у него свой сетевой namespace. Чтобы достучаться до сервиса на хосте с Windows/Mac, используй host.docker.internal (Docker Desktop) или –add-host=host.docker.internal:host-gateway при запуске.

Медленная сборка образа

  • Неправильный порядок COPY
  • Нет .dockerignore
  • Не используется кеш слоёв

Практические сниппеты по теме

На сайте есть готовые сниппеты с разбором команд, сетей и хранения данных:

  • Docker: разница между run, start и exec — когда создавать контейнер, когда запускать остановленный, как войти в уже работающий
  • Docker networking: почему localhost не работает между контейнерами — embedded DNS, имя сервиса вместо 127.0.0.1, пример docker-compose
  • Как уменьшить размер Docker-образа: alpine, кеш и порядок инструкций — Alpine vs Debian, кеширование слоёв, правильный порядок COPY в Dockerfile
  • Docker Compose: volumes vs bind mounts — когда bind mount (разработка), когда volume (production), пример для обоих вариантов
  • Docker system prune — очистка неиспользуемых образов, контейнеров и томов

Итог

Docker — это:

  • не DevOps-магия,
  • не Kubernetes,
  • не оверинженерия.

Это инструмент разработчика , который:

  • упрощает локальную разработку,
  • убирает «у меня работает»,
  • делает деплой предсказуемым.

Если раньше ты делал VM — Docker станет логичным следующим шагом.

Не сразу идеально, но один раз правильно.

Read more on viku-lov.ru