Embedded HandbookПриложения
Все разделы › XII. Сети и DevOps для инженера
STM32F103, STM32F407 · уровень средний · 8 мин чтения
Открыть в интерактивном справочнике

111. Контейнеры и инфраструктура: Compose, образы, обратный прокси

Прибор редко живёт один: данные уходят на сервер, где их принимают, хранят и показывают. Этот сервер — тоже часть изделия, и собирать его руками на каждой машине так же плохо, как собирать прошивку без конвейера (раздел 110). Раздел строит маленькую, но настоящую инфраструктуру: приёмник телеметрии на C в контейнере, обратный прокси перед ним и файл Compose, который поднимает всё одной командой. Каждое свойство проверено опытом: стенд поднимается, его роняют, пересоздают и проверяют, что видно снаружи.

Как это называется в документации

Оригинал Перевод
image, layer образ, слой образа
container контейнер: запущенный образ
multi-stage build многоступенчатая сборка образа
volume / bind mount том / подключённый каталог хоста
Compose file, service файл Compose, сервис
healthcheck проверка здоровья контейнера
restart policy правило перезапуска
reverse proxy обратный прокси
rate limiting ограничение частоты запросов
internal network внутренняя сеть без выхода наружу

111.1 Что строим

Прибор обращается к прокси; прокси в двух сетях, приёмник только во внутренней, данные в томе

Приборы шлют показания запросом HTTP POST /api/telemetry (раздел 107), приёмник дописывает строку в файл и по запросу говорит, сколько строк принято. Приёмник — небольшая программа на C без библиотек: так видно, что контейнеру всё равно, на чём написана программа.

Наружу открыт только прокси nginx. Он принимает запросы, отсекает лишнее и передаёт остальное приёмнику во внутреннюю сеть. Три свойства, ради которых всё затевается:


111.2 Образ: две ступени

Образ собирается из Dockerfile. Компилятор нужен только чтобы собрать программу, в работающем образе ему делать нечего:

FROM debian:12-slim AS build
COPY receiver.c /src/receiver.c
RUN gcc -std=gnu11 -O2 -Wall -Wextra -static -o /receiver /src/receiver.c \
 && mkdir /data

FROM scratch
COPY --from=build /receiver /receiver
ENTRYPOINT ["/receiver"]

Первая ступень — Debian с gcc, вторая — FROM scratch, пустой образ, куда копируется одна статически собранная программа. Итог опыта: образ приёмника — 351 346 байт, ступень с компилятором — 98 МБ, в 279 раз больше. Меньше образ — быстрее выкладка по медленному каналу и меньше программ, в которых может найтись уязвимость: в scratch нет даже оболочки.

Двухступенчатая сборка: компилятор остаётся в первой ступени, в образ уходит одна программа

Номер версии базового образа закрепляют так же, как версию компилятора в конвейере: nginx:1.31.3-alpine, а не nginx:latest, — иначе завтрашняя сборка выйдет не такой, как сегодняшняя. Но и тег с версией (nginx:1.31.3-alpine, debian:12-slim) пересобирают с обновлениями безопасности: содержимое меняется, имя — нет. Неизменен только хэш образа @sha256:….


111.3 Не от root

Процесс в контейнере по умолчанию работает от root. Приёмнику это не нужно:

COPY --from=build --chown=65534:65534 /data /data
USER 65534:65534

Опыт подтвердил: контейнер работает от пользователя 65534. Каталог /data создаётся в образе сразу с этим владельцем — новый том получает те же права, и программа может писать в свой файл, но больше никуда.


111.4 Compose: весь стенд в одном файле

Файл compose.yaml описывает сервисы, тома и сети:

services:
  receiver:
    build: ./receiver
    restart: on-failure
    volumes:
      - telemetry:/data
    networks:
      - back
  proxy:
    image: nginx:1.31.3-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:18111:80"
    volumes:
      - ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - front
      - back
volumes:
  telemetry:
networks:
  front:
  back:
    internal: true

Прокси подключён к обеим сетям, приёмник — только к back. Именованный том объявляют в корне файла: без раздела volumes Compose откажется запускаться («refers to undefined volume»). Проверка здоровья и порядок запуска — в 111.5.

Команды, которыми пользуются каждый день:

docker compose up -d --build     собрать и поднять в фоне
docker compose ps                что запущено и в каком состоянии
docker compose logs -f receiver  журнал сервиса
docker compose down              остановить и удалить контейнеры
docker compose down -v           то же вместе с томами — и данными

Порт прокси опубликован только на 127.0.0.1: на сервере наружу его выпускает сетевой экран или внешний прокси с шифрованием, а не Docker сам. Порт, опубликованный на всех адресах ("18111:80"), Docker пробрасывает своими правилами в iptables ещё до того, как пакет дойдёт до правил ufw (и зон firewalld): порт виден из сети, даже если сетевой экран его «запрещает». Ограничения для опубликованных портов ставят в цепочку DOCKER-USER — её Docker проверяет раньше своих правил.


111.5 Проверка здоровья и порядок запуска

«Контейнер запущен» не значит «программа готова отвечать». Проверка здоровья спрашивает саму программу. В образе scratch нет ни curl, ни оболочки — поэтому приёмник умеет проверять себя сам:

    if (argc > 1 && strcmp(argv[1], "--check") == 0)
        return self_check();

self_check() делает запрос GET /api/count к себе и возвращает 0 при ответе 200. В Compose:

  receiver:
    healthcheck:
      test: ["CMD", "/receiver", "--check"]
      interval: 2s
  proxy:
    depends_on:
      receiver:
        condition: service_healthy

Прокси запускается только после того, как приёмник стал healthy: первые запросы приборов не получат ошибку «сервис недоступен».


111.6 Падение и перезапуск

Правило restart: on-failure поднимает контейнер, если процесс завершился с ошибкой. Опыт: служебный запрос изнутри сети заставил приёмник упасть. Docker перезапустил его сам — RestartCount стал 1, — проверка здоровья снова зелёная, а счётчик принятых строк остался 3: они лежат в файле на томе, а не в памяти процесса.

unless-stopped у прокси поднимает его и после перезагрузки сервера. Перезапуск — страховка, а не лечение: частые перезапуски видны в RestartCount и должны превращаться в оповещение (раздел 112).


111.7 Тома: данные отдельно от контейнера

Контейнер одноразовый: его удаляют при каждом обновлении образа. Данные живут в томе telemetry, подключённом к /data. Опыт:

Поэтому down -v на рабочем сервере — команда, после которой нужна резервная копия. Том для важных данных копируют по расписанию — например, отдельным контейнером с tar, подключённым к тому же тому.


111.8 Сети: наружу только прокси

Приёмник не публикует ни одного порта и подключён только к сети back с internal: true — у неё нет выхода наружу. Прокси подключён к обеим сетям. Опыт: контейнер во внешней сети front до приёмника не достучался, а контейнер в сети back — достучался. Внутри сети сервисы находят друг друга по имени: прокси обращается к receiver:8080, адресов никто не пишет.

Если приёмник взломают, из внутренней сети ему некуда отправить данные; если сломают прокси — он видит только приёмник, а не всё остальное на сервере.


111.9 Обратный прокси

limit_req_zone $binary_remote_addr zone=tele:1m rate=5r/s;

location = /api/crash {
    return 403;
}

location /api/ {
    limit_req zone=tele burst=5 nodelay;
    limit_req_status 429;
    client_max_body_size 1k;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_pass http://receiver:8080;
}

Что прокси сделал в опыте:

Запрос Ответ
три строки на /api/telemetry 200, приёмник насчитал 3
служебный /api/crash 403 — наружу не выпущен
всё вне /api/ 404
тело больше 1 КБ 413
20 запросов подряд прошли 6–7, остальные — 429

Ограничение — 5 запросов в секунду с запасом 5: сломанный прибор, который шлёт без остановки, не положит приёмник. Приёмник видит соединение от прокси, а настоящий адрес клиента получает в заголовке X-Real-IP: через прокси /api/whoami вернул адрес клиента, напрямую — прочерк. На рабочем сервере прокси же держит сертификат HTTPS — приёмник о шифровании не знает.


111.10 Грабли

Контейнер «работает», а сервис не отвечает. Нет проверки здоровья, зависимые сервисы стартуют раньше. healthcheck и condition: service_healthy.

После обновления пропали данные. Данные писались внутрь контейнера, а не в том. Всё, что должно пережить контейнер, — в volumes.

permission denied при записи в том. Программа работает не от root, а каталог принадлежит root. Создать каталог в образе с нужным владельцем.

Сборка вчера и сегодня дала разные образы. Базовый образ latest. Закреплять версии.

Приёмник доступен из интернета в обход прокси. Опубликован его порт. Публиковать только прокси, приёмник — во внутренней сети.

В журнале приёмника у всех один и тот же адрес. Это адрес прокси. Брать X-Real-IP, а прокси — ставить его.

down -v на сервере. Удаляет тома с данными. Резервная копия — до.


111.11 Итоги

Дальше: раздел 112 «Наблюдаемость» — журналы, метрики и оповещения для этого стенда.

Открыть в интерактивном справочнике