Docker для фронтенд- и бэк-разработчиков: практический гайд без DevOps-магии
Практический гайд по Docker: контейнеры, Dockerfile, Docker Compose, деплой и типовые ошибки. Примеры для Node.js, PHP, WordPress, PostgreSQL. Без оверинженеринга — для разработчиков.
Требования
- Linux / macOS / Windows (WSL2)
- Базовое понимание командной строки
- Опыт разработки на PHP или Node.js
Docker для фронтенд- и бэк-разработчиков: практический гайд без DevOps-магии
Docker — это не «магия для DevOps», а нормальный инженерный инструмент, который решает очень приземлённую проблему: один и тот же проект должен одинаково запускаться локально, у коллеги и на сервере.
Если ты фронтендер, бэкендер, PHP- или Node-разработчик — Docker нужен тебе ровно для этого. Не для Kubernetes, не для резюме, не для пафоса.
Ниже — честный и практический гайд. Строго по документации, с примерами из реальной разработки.
Что такое контейнер и почему он лучше VM
Контейнер простыми словами
Контейнер — это изолированный процесс в Linux с:
- собственным файловым пространством,
- своей сетью,
- своими зависимостями.
Контейнер не содержит своей операционной системы — только процесс(ы) и файлы из образа.
Контейнер vs VirtualBox / KVM — на одном примере
Задача: запустить Node.js + PostgreSQL для проекта.
Виртуальная машина (VirtualBox / KVM)
- Создаёшь VM с Ubuntu
- Ставишь Node.js
- Ставишь PostgreSQL
- Ловишь конфликт версий
- Настраиваешь systemd
- VM весит 2–4 ГБ
- Запускается минутами
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)
- Установить Docker Desktop
- Включить WSL2
- 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 — конфиги
Контейнеры можно удалять, данные — нет.
Обновление без простоя
- Поднять новый контейнер
- Переключить трафик (nginx)
- Остановить старый
Для небольших проектов этого достаточно.
Про 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 станет логичным следующим шагом. Не сразу идеально, но один раз правильно.



Комментарии