Зачем Docker: "работает на моей машине"
История, знакомая каждому разработчику. Приложение работает на вашем ноутбуке. Выкладываете на сервер — падает. Отличия: версия Python 3.9 vs 3.11, разные системные библиотеки, другая переменная PATH, конфликт зависимостей с другим приложением.
Docker решает эту проблему радикально: упаковывает приложение вместе со всем окружением — операционной системой, библиотеками, конфигурацией. Контейнер запускается одинаково везде: на ноутбуке разработчика, в CI/CD, на production-сервере, в облаке.
Дополнительные бонусы:
Изоляция: одно приложение не мешает другому
Воспроизводимость: одинаковая среда у всей команды
Быстрое развёртывание:
docker pull+docker runвместо часа установкиМасштабирование: запустить 10 экземпляров так же легко, как 1
Чистота: удалить контейнер = никаких следов на хосте
Основные концепции
Image (образ): Слоёная файловая система со всем необходимым. Неизменяемый шаблон. Хранится в Registry (Docker Hub, GitHub Container Registry, ваш собственный).
Container (контейнер): Запущенный экземпляр образа. Изолированный процесс с собственной файловой системой, сетью, PID-пространством. Контейнеры ephemeral — данные исчезают при удалении (если нет Volume).
Volume (том): Постоянное хранилище данных, переживает удаление контейнера.
Network (сеть): Изолированная виртуальная сеть. Контейнеры в одной сети видят друг друга по имени.
Dockerfile: Инструкции для сборки образа. Каждая инструкция — новый слой.
Registry: Хранилище образов. Docker Hub — публичный. Можно развернуть свой (Harbor, Nexus).
Dockerfile: пишем правильно
Базовый пример (Python приложение):
# Начинаем с официального образа Python
# ВАЖНО: всегда указывайте точную версию, не :latest!
FROM python:3.11-slim
# Метаданные
LABEL maintainer="your@email.com"
LABEL version="1.0"
LABEL description="Industrial IoT Gateway"
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем ТОЛЬКО файлы зависимостей первыми!
# Слои кешируются — если requirements.txt не изменился,
# pip install не запустится при следующей сборке
COPY requirements.txt .
# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt
# Копируем исходный код (изменяется чаще — поэтому последним)
COPY src/ ./src/
COPY config/ ./config/
# Создаём непривилегированного пользователя (безопасность!)
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser
# Переменные окружения
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
LOG_LEVEL=INFO
# Открываем порт (документация, не публикует сам по себе)
EXPOSE 8080
# Healthcheck: Docker проверяет живость контейнера
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Команда запуска
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080"]
Многоэтапная сборка (Multi-stage build):
Критически важна для production. Финальный образ не содержит инструментов сборки (gcc, make, pip), что уменьшает размер и поверхность атаки:
# ===== ЭТАП 1: Сборка =====
FROM python:3.11 AS builder
WORKDIR /build
# Устанавливаем зависимости для сборки
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
# Устанавливаем в отдельную директорию
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ===== ЭТАП 2: Production образ =====
FROM python:3.11-slim AS production
# Копируем только установленные пакеты из builder
COPY --from=builder /install /usr/local
WORKDIR /app
# Только runtime зависимости ОС
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Безопасность
RUN groupadd -r app && useradd -r -g app -d /app -s /sbin/nologin app
COPY --chown=app:app src/ ./src/
USER app
ENV PYTHONUNBUFFERED=1
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:8080/health
CMD ["python", "src/main.py"]
# Результат: образ ~180MB вместо ~900MB!
.dockerignore — что НЕ копировать в образ:
# .dockerignore
**/__pycache__
**/*.pyc
**/*.pyo
.git
.gitignore
.venv
venv
*.env
.env.*
tests/
docs/
*.md
.github/
node_modules/
dist/
*.log
.DS_Store
# Секреты — никогда в образ!
*.key
*.pem
*secret*
config.local.*
Docker Compose: многосервисные приложения
Docker Compose — инструмент для запуска нескольких связанных контейнеров.
Полный пример: IoT-платформа
# docker-compose.yml
version: '3.9'
# Общие настройки через YAML anchors (DRY)
x-common-env: &common-env
TZ: Europe/Moscow
LOG_LEVEL: ${LOG_LEVEL:-INFO}
x-restart-policy: &restart-policy
restart: unless-stopped
services:
# ===== MQTT БРОКЕР =====
mosquitto:
image: eclipse-mosquitto:2.0.18
<<: *restart-policy
volumes:
- ./mosquitto/config:/mosquitto/config:ro
- mosquitto_data:/mosquitto/data
- mosquitto_logs:/mosquitto/log
ports:
- "1883:1883" # MQTT
- "9001:9001" # WebSocket
networks:
- iot_network
healthcheck:
test: ["CMD", "mosquitto_sub", "-t", "$$SYS/#", "-C", "1", "-i", "healthcheck"]
interval: 30s
timeout: 10s
retries: 3
# ===== БАЗА ДАННЫХ ВРЕМЕННЫХ РЯДОВ =====
influxdb:
image: influxdb:2.7
<<: *restart-policy
environment:
<<: *common-env
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USER:-admin}
DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD:?INFLUX_PASSWORD required}
DOCKER_INFLUXDB_INIT_ORG: factory
DOCKER_INFLUXDB_INIT_BUCKET: telemetry
DOCKER_INFLUXDB_INIT_RETENTION: 30d
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN:?INFLUX_TOKEN required}
volumes:
- influxdb_data:/var/lib/influxdb2
- influxdb_config:/etc/influxdb2
ports:
- "8086:8086"
networks:
- iot_network
- monitoring_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8086/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
# ===== GRAFANA =====
grafana:
image: grafana/grafana:10.3.1
<<: *restart-policy
depends_on:
influxdb:
condition: service_healthy
environment:
<<: *common-env
GF_SECURITY_ADMIN_USER: ${GF_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD:?required}
GF_SERVER_ROOT_URL: http://localhost:3000
GF_SMTP_ENABLED: "true"
GF_SMTP_HOST: ${SMTP_HOST:-localhost:25}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
ports:
- "3000:3000"
networks:
- monitoring_network
user: "472" # grafana user
# ===== IOT ШЛЮЗ (наш сервис) =====
gateway:
build:
context: .
dockerfile: Dockerfile
target: production
args:
BUILD_DATE: ${BUILD_DATE:-unknown}
GIT_COMMIT: ${GIT_COMMIT:-unknown}
<<: *restart-policy
depends_on:
mosquitto:
condition: service_healthy
influxdb:
condition: service_healthy
environment:
<<: *common-env
MQTT_HOST: mosquitto # Имя сервиса = DNS-имя внутри сети!
MQTT_PORT: 1883
INFLUX_URL: http://influxdb:8086
INFLUX_TOKEN: ${INFLUX_TOKEN}
INFLUX_ORG: factory
INFLUX_BUCKET: telemetry
MODBUS_PORT: /dev/ttyUSB0 # Реальный порт с хоста
volumes:
- ./config:/app/config:ro
- gateway_logs:/app/logs
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0" # Проброс USB-устройства
ports:
- "8080:8080"
networks:
- iot_network
- monitoring_network
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
# ===== NGINX: реверс-прокси =====
nginx:
image: nginx:1.25-alpine
<<: *restart-policy
depends_on:
- grafana
- gateway
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro # TLS сертификаты
ports:
- "80:80"
- "443:443"
networks:
- monitoring_network
# ===== ТОМА =====
volumes:
mosquitto_data:
mosquitto_logs:
influxdb_data:
influxdb_config:
grafana_data:
gateway_logs:
# ===== СЕТИ =====
networks:
iot_network: # Для IoT-компонентов
driver: bridge
monitoring_network: # Для мониторинга и UI
driver: bridge
.env файл для Compose:
# .env — НЕ коммитить в git!
INFLUX_USER=admin
INFLUX_PASSWORD=super_secret_influx_pass
INFLUX_TOKEN=my-super-secret-token-change-this
GF_ADMIN_USER=admin
GF_ADMIN_PASSWORD=super_secret_grafana_pass
LOG_LEVEL=INFO
BUILD_DATE=2024-03-15
GIT_COMMIT=abc123
Docker: ключевые команды
# ===== ОБРАЗЫ =====
docker build -t myapp:1.0 . # Собрать образ
docker build --no-cache -t myapp:1.0 . # Без кеша
docker images # Список образов
docker rmi myapp:1.0 # Удалить образ
docker image prune # Удалить неиспользуемые
# История слоёв (анализ размера):
docker history myapp:1.0
# ===== КОНТЕЙНЕРЫ =====
docker run -d \
--name gateway \
-p 8080:8080 \
-e MQTT_HOST=192.168.1.100 \
-v $(pwd)/config:/app/config:ro \
--restart unless-stopped \
myapp:1.0
docker ps # Запущенные контейнеры
docker ps -a # Все (включая остановленные)
docker logs gateway -f # Логи в реальном времени
docker logs gateway --tail 100 # Последние 100 строк
docker exec -it gateway bash # Войти внутрь контейнера
docker stop gateway # Остановить
docker start gateway # Запустить
docker restart gateway # Перезапустить
docker rm gateway # Удалить (сначала stop)
docker stats # Потребление ресурсов
# ===== COMPOSE =====
docker compose up -d # Запустить все сервисы
docker compose up -d gateway # Запустить только gateway
docker compose down # Остановить и удалить контейнеры
docker compose down -v # + удалить тома (ОСТОРОЖНО!)
docker compose logs -f # Логи всех сервисов
docker compose logs -f gateway # Логи конкретного сервиса
docker compose ps # Статус сервисов
docker compose pull # Обновить образы
docker compose build --no-cache # Пересобрать
docker compose restart gateway # Перезапустить сервис
docker compose exec gateway bash # Войти в контейнер
# ===== REGISTRY =====
docker tag myapp:1.0 ghcr.io/myorg/myapp:1.0
docker push ghcr.io/myorg/myapp:1.0
docker pull ghcr.io/myorg/myapp:1.0
# ===== ОЧИСТКА =====
docker system prune -a # Удалить ВСЁ неиспользуемое
docker volume prune # Удалить неиспользуемые тома
Volumes и persistence: не теряем данные
# Типы монтирования:
services:
app:
volumes:
# 1. Named Volume (рекомендуется для данных БД)
- db_data:/var/lib/postgresql/data
# 2. Bind Mount (для конфигов и разработки)
- ./config:/app/config:ro # :ro = read-only
- ./src:/app/src # Для hot-reload при разработке
# 3. tmpfs (только в RAM, для временных данных)
- type: tmpfs
target: /tmp
tmpfs:
size: 100M
volumes:
db_data:
driver: local
# Для production: внешние тома
# external: true
# name: prod_db_data
Backup томов:
# Резервная копия тома influxdb_data
docker run --rm \
-v influxdb_data:/source:ro \
-v $(pwd)/backups:/backup \
alpine:3 \
tar czf /backup/influxdb_$(date +%Y%m%d).tar.gz -C /source .
# Восстановление
docker run --rm \
-v influxdb_data:/target \
-v $(pwd)/backups:/backup:ro \
alpine:3 \
tar xzf /backup/influxdb_20240315.tar.gz -C /target
Оптимизация размера образа
# ❌ ПЛОХО: большой образ FROM ubuntu:22.04 RUN apt-get update RUN apt-get install -y python3 RUN apt-get install -y python3-pip RUN pip3 install flask COPY app.py . CMD ["python3", "app.py"] # Размер: ~480 МБ, 5 лишних слоёв #
✅ ХОРОШО: оптимизированный образ FROM python:3.11-slim RUN pip install --no-cache-dir flask COPY app.py . CMD ["python", "app.py"] # Размер: ~85 МБ, чистая сборка #
✅ ЕЩЁ ЛУЧШЕ: alpine (минимальный дистрибутив) FROM python:3.11-alpine # Некоторые C-расширения нужно собирать RUN apk add --no-cache gcc musl-dev && \ pip install --no-cache-dir flask && \ apk del gcc musl-dev COPY app.py . CMD ["python", "app.py"] # Размер: ~45 МБ
Правило минимума слоёв для apt/apk:
# Объединяйте RUN команды в одну для минимизации слоёв
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
libpq5 \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# ОДИН слой вместо нескольких, и сразу очистка кеша
Безопасность Docker
1. Никогда не запускать от root:
# Создаём непривилегированного пользователя
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
2. Read-only файловая система:
services:
app:
read_only: true # Файловая система только для чтения
tmpfs:
- /tmp # Разрешаем запись только в tmpfs
- /var/run
3. Ограничение capabilities:
services:
app:
cap_drop:
- ALL # Убираем ВСЕ capabilities
cap_add:
- NET_BIND_SERVICE # Добавляем только необходимые
4. Сканирование образов на уязвимости:
# Trivy — бесплатный сканер (Aqua Security)
docker run --rm aquasec/trivy image myapp:1.0
# Или встроенный Docker Scout
docker scout cves myapp:1.0
5. Secrets — не в переменных окружения production:
# docker-compose.yml с Docker secrets
services:
app:
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password # Читаем из файла!
secrets:
db_password:
file: ./secrets/db_password.txt # или external: true для Swarm/K8s
Healthcheck и зависимости между сервисами
services:
postgres:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # Даём время на инициализацию
app:
depends_on:
postgres:
condition: service_healthy # Ждём пока postgres healthy!
redis:
condition: service_healthy
Entrypoint-скрипт для ожидания зависимостей:
#!/bin/sh
# entrypoint.sh
set -e
echo "Ожидание готовности базы данных..."
until nc -z -w5 ${DB_HOST} ${DB_PORT}; do
echo "База данных недоступна, ждём..."
sleep 2
done
echo "База данных готова!"
# Запуск миграций
python manage.py migrate --no-input
# Запуск приложения
exec "$@"
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "app:application"]
Docker в CI/CD (GitHub Actions)
# .github/workflows/docker.yml
name: Build and Deploy
on:
push:
tags: ['v*.*.*']
jobs:
build-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64 # Multi-arch!
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
GIT_COMMIT=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
deploy:
needs: build-push
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/iot-platform
# Обновляем образ
docker compose pull gateway
# Обновляем с нулевым downtime
docker compose up -d --no-deps gateway
# Ждём healthcheck
sleep 15
docker compose ps gateway | grep -q "healthy" || exit 1
echo "Деплой успешен!"
Заключение: когда Docker, когда нет
Используйте Docker:
Приложения с множеством зависимостей
Многосервисные приложения (backend + БД + брокер + мониторинг)
CI/CD с гарантированной воспроизводимостью
Несколько приложений на одном сервере
Нужна возможность быстрого масштабирования
Не обязательно Docker:
Простые скрипты без зависимостей
Приложения с прямым доступом к оборудованию (хотя
--deviceпомогает)Жёсткое реальное время (latency контейнера ~1 мкс, но это не ноль)
Команда незнакома с Docker — сначала обучение, потом внедрение
Docker — это не серебряная пуля, но инвестиция в него окупается быстро. Начните с docker-compose.yml для локальной разработки — уже это даст ощутимый результат: одна команда docker compose up поднимает весь стек вместо часа настройки.
Create an account or sign in to leave a review
There are no reviews to display.