Руководство по написанию Dockerfile
Введение
Dockerfile — это файл с инструкциями для создания Docker-образа. Простыми словами, это «рецепт для развёртывания приложения на сервере» — только выполняет его не человек, а сам Docker.
Почему важно уметь писать Dockerfile?
- Воспроизводимая среда — образ, собранный из Dockerfile, работает одинаково на любом сервере. Классическая проблема «у меня на компьютере работает» исчезает.
- Контроль версий — Dockerfile хранится вместе с кодом в Git. Можно отследить, кто, когда и что изменил.
- Автоматизация — в CI/CD-пайплайнах Docker-образы автоматически собираются и деплоятся.
В данном руководстве мы рассмотрим все основные инструкции Dockerfile, multi-stage сборку, кэширование слоёв, лучшие практики безопасности и готовые примеры для продакшена.
Инструкции Dockerfile
FROM — Выбор базового образа
Каждый Dockerfile начинается с FROM. Эта инструкция указывает, на основе какого базового образа будет строиться ваш образ.
FROM <image>[:<tag>] [@<digest>]Примеры:
# Последняя версия (не рекомендуется — неоднозначно)
FROM node
# Конкретная версия (рекомендуется)
FROM node:20-alpine
# По digest (самый точный вариант — на основе хеша содержимого образа)
FROM node@sha256:a1b2c3d4...Важные правила для продакшена:
- Никогда не используйте тег
latest— образ, который работает сегодня, может измениться на другую версию завтра, и ваш проект сломается. - Отдавайте предпочтение Alpine-вариантам — разница между
node:20-alpine(50 МБ) иnode:20(350 МБ) колоссальная. Меньший образ = быстрая сборка, меньше уязвимостей. - Указывайте точную версию — используйте теги вроде
python:3.12-slim,golang:1.22-alpine.
LABEL — Метаданные образа
Инструкция LABEL добавляет метаданные к образу. Она заменяет устаревшую инструкцию MAINTAINER.
FROM node:20-alpine
LABEL maintainer="Otabek Ismoilov <ismoilovdev@gmail.com>"
LABEL version="1.0"
LABEL description="DevOps Journey API service"Посмотреть метки образа: docker inspect --format='{{json .Config.Labels}}' image_name
WORKDIR — Рабочая директория
WORKDIR задаёт рабочую директорию внутри контейнера. Все последующие инструкции RUN, COPY, CMD и другие будут выполняться в этой директории.
FROM python:3.12-slim
WORKDIR /app
# Теперь команды COPY и RUN работают в директории /app
COPY requirements.txt .
RUN pip install -r requirements.txtИспользуйте WORKDIR /app, а не RUN cd /app. Причина: RUN cd /app действует только в рамках одной инструкции RUN — следующая инструкция вернётся в корневую директорию. WORKDIR же применяется ко всем последующим инструкциям.
Если директория не существует, Docker создаст её автоматически.
COPY и ADD — Копирование файлов
COPY — копирует файлы с хост-машины в образ. Просто и понятно.
# Скопировать один файл
COPY package.json /app/
# Скопировать все файлы
COPY . /app/
# Скопировать несколько файлов
COPY package.json package-lock.json /app/ADD — работает так же, как COPY, но имеет дополнительные возможности:
- Загрузка файлов по URL
- Автоматическая распаковка архивов
.tar.gz
# Автоматически распакует архив
ADD app.tar.gz /app/
# Загрузит файл по URL
ADD https://example.com/config.json /app/Правило: Если не нужна распаковка архивов или загрузка по URL — всегда используйте COPY. «Скрытые» возможности ADD могут привести к неожиданным результатам. В большинстве случаев COPY достаточно и безопаснее.
RUN — Выполнение команд
Инструкция RUN выполняет команды во время сборки образа — установка пакетов, подготовка файлов, компиляция и прочее.
FROM ubuntu:24.04
# Плохая практика — каждый RUN создаёт отдельный слой
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean
# Хорошая практика — один слой, образ меньше
RUN apt-get update && apt-get install -y \
curl \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*Зачем объединять команды?
Каждая инструкция (RUN, COPY, ADD) в Dockerfile добавляет новый слой к образу. Больше слоёв = больше размер образа. Поэтому:
- Объединяйте связанные команды через
&& - Очищайте кэш с помощью
apt-get cleanиrm -rf /var/lib/apt/lists/* - Удаляйте ненужные файлы в том же слое (удаление в следующем слое не уменьшит размер образа!)
ENV — Переменные окружения
ENV задаёт переменные окружения внутри контейнера. Эти значения доступны как во время сборки, так и при работе контейнера.
FROM node:20-alpine
ENV NODE_ENV=production
ENV PORT=3000
# Значения ENV можно использовать в последующих инструкциях
EXPOSE $PORT
CMD ["node", "server.js"]Значения, заданные через ENV, можно переопределить при запуске контейнера: docker run -e PORT=4000.
ARG — Переменные времени сборки
ARG определяет переменные, доступные только во время сборки. При запуске контейнера они недоступны.
# ARG со значением по умолчанию
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
ARG APP_VERSION=1.0.0
LABEL version="${APP_VERSION}"Переопределение ARG при сборке:
docker build --build-arg NODE_VERSION=18 --build-arg APP_VERSION=2.0.0 -t myapp .Разница между ARG и ENV:
ARG— работает только во время сборки, не виден внутри контейнераENV— работает и во время сборки, и внутри работающего контейнера
Никогда не передавайте секреты (пароли, токены) через ARG! Они будут видны в истории сборки (docker history).
CMD и ENTRYPOINT — Запуск контейнера
Эти две инструкции определяют, какая команда выполняется при запуске контейнера.
CMD — команда по умолчанию. Переопределяется, если при docker run передана другая команда.
FROM python:3.12-slim
CMD ["python", "app.py"]# Выполняется CMD — python app.py
docker run myapp
# CMD переопределяется — запускается bash
docker run myapp bashENTRYPOINT — основная команда. Не переопределяется, к ней только добавляются аргументы.
FROM python:3.12-slim
ENTRYPOINT ["python", "app.py"]# Выполняется ENTRYPOINT — python app.py
docker run myapp
# Добавляется аргумент — python app.py --debug
docker run myapp --debugENTRYPOINT + CMD вместе — самая мощная комбинация:
FROM nginx:alpine
# ENTRYPOINT — основная команда (не меняется)
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# CMD — аргументы по умолчанию (можно переопределить)
CMD ["-c", "/etc/nginx/nginx.conf"]# С конфигом по умолчанию: nginx -g "daemon off;" -c /etc/nginx/nginx.conf
docker run mynginx
# С другим конфигом: nginx -g "daemon off;" -c /custom/nginx.conf
docker run mynginx -c /custom/nginx.confКогда что использовать?
- Только
CMD— для простых приложений, когда нужно запускать различные команды внутри контейнера ENTRYPOINT+CMD— для продакшен-сервисов. ENTRYPOINT запускает основное приложение, CMD задаёт параметры по умолчанию- Только
ENTRYPOINT— когда контейнер должен выполнять строго одну конкретную программу
EXPOSE — Документация портов
EXPOSE сообщает Docker, какие порты слушает контейнер. Однако эта инструкция не открывает порт — она служит исключительно документацией.
FROM node:20-alpine
EXPOSE 3000
CMD ["node", "server.js"]Чтобы фактически открыть порт, используйте docker run -p:
# Привязать порт 3000 контейнера к порту 8080 хоста
docker run -p 8080:3000 myapp
# Открыть все порты, указанные в EXPOSE
docker run -P myappVOLUME — Постоянные данные
VOLUME создаёт точку монтирования для хранения постоянных (persistent) данных внутри контейнера. Данные в томе сохраняются даже после удаления контейнера.
FROM postgres:16-alpine
# Файлы базы данных PostgreSQL хранятся здесь
VOLUME /var/lib/postgresql/dataПодключение тома при запуске:
# Монтирование директории хоста
docker run -v /host/data:/var/lib/postgresql/data postgres
# Именованный том Docker
docker run -v pgdata:/var/lib/postgresql/data postgresКогда использовать VOLUME?
- Базы данных (PostgreSQL, MySQL, MongoDB)
- Файлы логов
- Загружаемые файлы
- Данные, разделяемые между контейнерами
USER — Безопасный пользователь
Инструкция USER указывает, от имени какого пользователя будут выполняться команды внутри контейнера.
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --only=production
# Переключение с root на пользователя node
USER node
EXPOSE 3000
CMD ["node", "server.js"]Никогда не запускайте контейнеры от root в продакшене!
Если в контейнере обнаружится уязвимость, контейнер с root-правами может дать злоумышленнику доступ и к хост-серверу. Всегда переключайтесь на непривилегированного пользователя с помощью инструкции USER.
HEALTHCHECK — Проверка здоровья контейнера
HEALTHCHECK указывает Docker, как проверять, что приложение внутри контейнера работает корректно.
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --only=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --spider --quiet http://localhost:3000/health || exit 1
CMD ["node", "server.js"]Параметры:
--interval=30s— проверка каждые 30 секунд--timeout=5s— неудача, если ответ не получен в течение 5 секунд--start-period=10s— 10 секунд ожидания для запуска приложения--retries=3— после 3 неудачных попыток подряд контейнер помечается какunhealthy
# Просмотр состояния здоровья контейнера
docker ps
# В столбце STATUS: healthy, unhealthy или startingЗачем нужен HEALTHCHECK?
Контейнер может «работать», но приложение внутри него — выдавать ошибки или зависнуть. С помощью HEALTHCHECK Docker (и оркестраторы — Kubernetes, Docker Swarm) обнаруживает проблему и перезапускает контейнер.
.dockerignore — Исключение ненужных файлов
Файл .dockerignore указывает, какие файлы не должны попадать в контекст сборки Docker. Работает аналогично .gitignore.
# Система контроля версий
.git
.gitignore
# Зависимости (будут переустановлены внутри образа)
node_modules
vendor
__pycache__
# Файлы IDE и ОС
.vscode
.idea
*.swp
.DS_Store
Thumbs.db
# Файлы Docker
Dockerfile
docker-compose.yml
.dockerignore
# Секретные файлы
.env
.env.local
*.pem
*.key
# Тесты и документация
tests
docs
README.md
LICENSEПоследствия отсутствия .dockerignore:
node_modules(сотни МБ) каждый раз копируется в контекст сборки — сборка замедляется- Директория
.git(вся история) попадает в образ — размер увеличивается - Файл
.env(пароли, токены) попадает в образ — угроза безопасности! - Любое изменение файла инвалидирует кэш Docker — сборка начинается заново
Multi-stage сборка
Multi-stage сборка — важнейшая техника написания профессиональных Dockerfile. Она позволяет создавать несколько этапов (stage) в одном Dockerfile. В итоге финальный образ содержит только необходимые файлы.
Проблема: Для компиляции Go-приложения нужен компилятор Go (1 ГБ+). Но для запуска скомпилированного бинарника компилятор не нужен. При использовании одного этапа ненужный компилятор остаётся в финальном образе.
Решение: Multi-stage сборка.
# ===== ЭТАП 1: Сборка =====
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# ===== ЭТАП 2: Продакшен =====
FROM alpine:3.19
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/server .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --spider --quiet http://localhost:8080/health || exit 1
ENTRYPOINT ["./server"]Результат: Образ сборки ~1 ГБ, финальный образ ~15 МБ!
Преимущества multi-stage сборки:
- Меньший размер образа — в финальном образе только бинарник и среда выполнения
- Безопасность — исходный код, инструменты сборки и тестовые файлы не попадают в финальный образ
- Скорость — меньший образ быстрее скачивается и деплоится
Кэширование слоёв — Ускорение сборки
Docker кэширует каждую инструкцию как отдельный слой. Если инструкция не изменилась, Docker берёт её из кэша, не выполняя заново. Правильное использование этого механизма ускоряет сборку в несколько раз.
Плохой порядок — при изменении кода все зависимости устанавливаются заново:
FROM node:20-alpine
WORKDIR /app
# При изменении любого файла npm install запустится заново
COPY . .
RUN npm install
CMD ["node", "server.js"]Хороший порядок — файлы зависимостей копируются отдельно:
FROM node:20-alpine
WORKDIR /app
# Шаг 1: Копирование только файлов зависимостей
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Шаг 2: Копирование кода приложения
COPY . .
CMD ["node", "server.js"]Почему это быстрее?
package.json меняется редко, а код приложения — часто. При таком порядке:
- При изменении кода — сборка продолжается с шага
COPY . . npm ciберётся из кэша (занимает секунды)- При изменении зависимостей —
npm ciвыполняется заново
Главное правило: То, что меняется реже, размещайте в начале Dockerfile, а то, что меняется чаще, — в конце.
Готовые Dockerfile для продакшена
Node.js (Express/NestJS)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# --- Продакшен ---
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --spider --quiet http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]Python (FastAPI/Django)
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# --- Продакшен ---
FROM python:3.12-slim
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Go (Gin/Fiber)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server .
# --- Продакшен ---
FROM alpine:3.19
RUN apk --no-cache add ca-certificates \
&& addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/server .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --spider --quiet http://localhost:8080/health || exit 1
ENTRYPOINT ["./server"]Java Spring Boot (Maven)
FROM maven:3.9-eclipse-temurin-21-alpine AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
# --- Продакшен ---
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget --spider --quiet http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"].NET (ASP.NET Core)
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS builder
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
# --- Продакшен ---
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/publish .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --spider --quiet http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "MyApp.dll"]Rust
FROM rust:1.77-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
COPY src ./src
RUN cargo build --release
# --- Продакшен ---
FROM alpine:3.19
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/target/release/myapp .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --spider --quiet http://localhost:8080/health || exit 1
ENTRYPOINT ["./myapp"]Лучшие практики — Итоги
| Правило | Плохо | Хорошо |
|---|---|---|
| Базовый образ | FROM node | FROM node:20-alpine |
| Кэширование слоёв | COPY . . затем RUN npm install | COPY package*.json . затем RUN npm ci |
| Пользователь | root (по умолчанию) | USER appuser |
| Сборка | Одноэтапная | Multi-stage сборка |
| Здоровье | Нет HEALTHCHECK | HEALTHCHECK --interval=30s ... |
| Размер | Кэш не очищен | rm -rf /var/lib/apt/lists/* |
| Секреты | .env в образе | .dockerignore + runtime env |
| Инструкции | Много строк RUN | Объединены через && |
Дополнительные примеры Dockerfile можно найти в репозитории devops-tools (opens in a new tab).
Дополнительные ресурсы
Дополнительные ресурсы
- Официальная документация Docker по Dockerfile (opens in a new tab)
- Официальные лучшие практики Docker (opens in a new tab)
- Установка Docker на серверы Linux (opens in a new tab)
Дата: 25.11.2023 (25 ноября 2023 г.)
Последнее обновление: 12.02.2026 (12 февраля 2026 г.)
Автор: Отабек Исмоилов
| Telegram (opens in a new tab) | GitHub (opens in a new tab) | LinkedIn (opens in a new tab) |
|---|