Главная/Работа с Images/Урок

3.3. Tags и digests

Цели

После этого материала вы сможете:

  • объяснить, почему tag не является идентификатором версии, и показать это экспериментально;
  • объяснить разницу между digest манифеста и digest index у multi-platform образов;
  • управлять pull policy и точно знать, когда Docker обращается к registry;
  • закрепить версию образа по digest в Dockerfile и в compose.yaml;
  • объяснить, почему docker pull может не обновить образ, и заставить его обновиться;
  • выбрать стратегию закрепления версий под конкретную задачу.

Предварительные знания

Ключевые термины

ТерминОбъяснение
tagИзменяемая метка, указывающая на манифест или index внутри repository
digestНеизменяемый идентификатор: SHA-256 содержимого манифеста или index
pull policyПравило, определяющее, когда обращаться к registry за образом
pinningЗакрепление конкретной версии образа
mutable tagТег, который может быть перепривязан к другому образу
immutable tagТег, перезапись которого запрещена настройками registry
floating tagПодвижный тег, намеренно указывающий на «текущую» версию: latest, stable, 3

Теория

Тег — это указатель, а не имя версии

В registry тег хранится как запись «имя → digest». Владелец repository может изменить эту запись в любой момент.

text
   до обновления                     после обновления
   ─────────────                     ────────────────
   3.13-slim ──► sha256:aaa...       3.13-slim ──► sha256:bbb...
   latest    ──► sha256:aaa...       latest    ──► sha256:bbb...
                                     (sha256:aaa... всё ещё существует,
                                      но на него ничто не указывает)

Это происходит регулярно и по хорошим причинам: выходит патч безопасности в базовом образе, и мейнтейнер пересобирает 3.13-slim, чтобы пользователи получили исправление. Версия Python осталась 3.13, но набор слоёв изменился.

Практическое следствие, которое трудно переоценить:

Утверждение «в production и в staging один и тот же образ, тег v2.1» ничего не гарантирует, если между развёртываниями прошло время.

Digest гарантирует содержимое

Digest вычисляется из содержимого манифеста. Его нельзя перепривязать: изменение содержимого даёт другой digest.

bash
python@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251

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

Цена: нечитаемость. Из строки не понять, что это Python 3.13. Отсюда стандартная практика — писать оба:

dockerfile
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251

Docker использует digest; тег остаётся для человека. Если тег и digest противоречат друг другу, приоритет у digest.

Два digest у multi-platform образа

Тонкость, которая регулярно сбивает с толку.

У multi-platform образа два уровня (урок 3.1):

text
   тег "python:3.13-slim"
         │
         ▼
   index (digest: sha256:INDEX...)          ← этот digest в RepoDigests
         ├── manifest amd64 (sha256:AMD...)  ← этот digest у образа на amd64
         └── manifest arm64 (sha256:ARM...)
DigestЧто идентифицируетГде встречается
index digestВесь multi-platform образdocker pull (строка Digest:), RepoDigests, docker images --digests
manifest digestОбраз для одной платформыdocker manifest inspect, ссылки внутри index

Для закрепления версии используется index digest: он работает на любой архитектуре, и Docker сам выберет нужный манифест. Закрепление по manifest digest привяжет вас к одной платформе — иногда это нужно, но обычно нет.

Pull policy: когда Docker идёт в registry

Три политики, задаваемые флагом --pull у docker run и ключом pull_policy в Compose:

ЗначениеПоведение
missing (по умолчанию)Обращаться к registry, только если образа нет локально
alwaysОбращаться всегда, проверяя актуальность
neverНикогда не обращаться; при отсутствии образа — ошибка

Значение по умолчанию объясняет самое частое недоразумение:

bash
docker run myapp:latest

Если myapp:latest уже загружен, Docker не проверит, не появилась ли новая версия. Он запустит то, что лежит локально, даже если в registry давно другой образ.

Именно поэтому в CI и в скриптах развёртывания добавляют явный docker pull или --pull=always.

Что делает docker pull при уже загруженном образе

docker pull python:3.13-slim при наличии образа локально:

  1. Запрашивает манифест по тегу.
  2. Сравнивает полученный digest с локальным.
  3. Совпал — выводит Image is up to date и завершается.
  4. Не совпал — скачивает недостающие слои и перепривязывает тег локально.

Шаг 4 важен: старый образ не удаляется. Он остаётся на диске без тега — становится dangling image. Отсюда постепенное заполнение диска у тех, кто регулярно обновляет образы. Разбирается в уроке 3.4.

Стратегии закрепления

СтратегияПримерВоспроизводимостьУдобствоКогда применять
Без тегаpythonнетвысокоеРазовые эксперименты
Floating tagpython:3низкаявысокоеЛокальные пробы
Minor-версияpython:3.13средняявысокоеРазработка
Полная версияpython:3.13.9-slimвыше среднейсреднееРазработка, тесты
Версия + digestpython:3.13-slim@sha256:...полнаянизкоеProduction, CI, базовые образы
Только digestpython@sha256:...полнаяочень низкоеАвтоматика

Практическая рекомендация курса:

  • базовые образы в Dockerfile — тег плюс digest, обновляется осознанно (можно автоматизировать через Renovate или Dependabot);
  • свои образы при развёртывании — по digest, полученному из сборки;
  • локальная разработка — тег, читаемость важнее.

Обоснование: Dockerfile определяет, что попадёт в ваш артефакт. Если базовый образ подменится незаметно, вы получите другой артефакт при том же коде — и это обнаружится в самый неподходящий момент.


Внутренний механизм

Как вычисляется digest

Digest — это sha256 от байтов JSON-документа манифеста, ровно в том виде, в котором он хранится в registry.

Отсюда неочевидное следствие: даже семантически идентичные манифесты дадут разные digest, если различаются форматированием или порядком ключей. Поэтому манифесты передаются и хранятся байт в байт — их нельзя «переформатировать».

При загрузке Docker вычисляет хэш полученного документа и сверяет с запрошенным. Несовпадение — ошибка, загрузка прерывается. Это защита от повреждения и подмены, работающая без подписей.

Почему локальные образы иногда не имеют digest

bash
docker build -t myapp:1.0 .
docker image inspect myapp:1.0 --format '{{.RepoDigests}}'
text
[]

Digest появляется только после взаимодействия с registry. Собранный локально образ ещё не имеет манифеста в том виде, в котором он будет опубликован, — точнее, у него нет привязки к repository в registry. После docker push digest появится.

Это объясняет, почему в CI digest берут из вывода docker push или из метаданных build-push-action, а не из локального inspect до публикации.


Команды и примеры

Тег и digest локально

bash
docker pull python:3.13-slim
text
3.13-slim: Pulling from library/python
Digest: sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
Status: Downloaded newer image for python:3.13-slim

Строка Digest: — это index digest.

bash
docker images python --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}'
text
REPOSITORY   TAG         DIGEST                       IMAGE ID
python       3.13-slim   sha256:bf503bb2243c5aad...   c2f1e0d9b8a7

Три разных идентификатора в одной строке. Ещё раз, чем они отличаются:

ПолеЧто этоОбласть
TAGИзменяемый указательRegistry
DIGESTХэш indexRegistry, неизменяем
IMAGE IDХэш конфигурацииЛокальная машина

Загрузка по digest

bash
DIGEST="$(docker image inspect python:3.13-slim \
    --format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "$DIGEST"

docker rmi python:3.13-slim
docker pull "python@$DIGEST"
text
sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
...
Status: Downloaded newer image for python@sha256:bf503bb2...

Проверим, что получили то же самое:

bash
docker images --digests | grep python
text
python   <none>   sha256:bf503bb2243c5aad...   c2f1e0d9b8a7   ...

Обратите внимание: TAG теперь <none>. Образ загружен по digest и не имеет тега. Он полностью работоспособен:

bash
docker run --rm "python@$DIGEST" python --version
text
Python 3.13.9

Присвоим ему тег для удобства:

bash
docker tag "python@$DIGEST" python:3.13-slim

Демонстрация изменяемости тега

Воспроизведём ситуацию, когда под одним тегом оказываются разные образы. Используем локальный registry.

bash
docker run -d --name reg -p 5000:5000 registry:2
sleep 2

mkdir -p /tmp/tagdemo && cd /tmp/tagdemo

# Версия 1
cat > Dockerfile <<'EOF'
FROM alpine:3.21
RUN echo "версия 1" > /version.txt
EOF
docker build -q -t localhost:5000/app:v1 . > /dev/null
docker push -q localhost:5000/app:v1

D1="$(docker image inspect localhost:5000/app:v1 \
      --format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "digest версии 1: $D1"

Теперь пересоберём под тем же тегом:

bash
cat > Dockerfile <<'EOF'
FROM alpine:3.21
RUN echo "версия 2 — другое содержимое" > /version.txt
EOF
docker build -q -t localhost:5000/app:v1 . > /dev/null
docker push -q localhost:5000/app:v1

D2="$(docker image inspect localhost:5000/app:v1 \
      --format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "digest версии 2: $D2"
text
digest версии 1: sha256:1a2b3c4d5e6f...
digest версии 2: sha256:7g8h9i0j1k2l...

Тег v1 не изменился, а digest — изменился. Проверим, что теперь получит новый пользователь:

bash
docker rmi localhost:5000/app:v1 > /dev/null
docker pull -q localhost:5000/app:v1 > /dev/null
docker run --rm localhost:5000/app:v1 cat /version.txt
text
версия 2 — другое содержимое

Тот, кто загрузил v1 вчера, и тот, кто загрузит сегодня, получат разные образы под одним тегом.

А по digest — всегда одно и то же:

bash
docker run --rm "localhost:5000/app@$D1" cat /version.txt
docker run --rm "localhost:5000/app@$D2" cat /version.txt
text
версия 1
версия 2 — другое содержимое

Первый образ никуда не делся: он существует и доступен по своему digest. Потерялся только указатель на него.

Именно на этом свойстве строится откат в разделе 14.

Pull policy на практике

bash
# образ есть локально — registry не опрашивается
docker run --rm localhost:5000/app:v1 cat /version.txt

# принудительная проверка актуальности
docker run --rm --pull=always localhost:5000/app:v1 cat /version.txt

# запрет обращения к registry
docker rmi localhost:5000/app:v1 > /dev/null
docker run --rm --pull=never localhost:5000/app:v1 cat /version.txt
text
docker: Error response from daemon: No such image: localhost:5000/app:v1

--pull=never полезен, когда нужно гарантировать использование локально собранного образа и исключить случайную загрузку из registry.

В Compose то же самое задаётся на уровне сервиса:

yaml
services:
  api:
    image: localhost:5000/app:v1
    pull_policy: always

Проверка обновления без загрузки

bash
docker pull python:3.13-slim

Повторно:

bash
docker pull python:3.13-slim
text
3.13-slim: Pulling from library/python
Digest: sha256:bf503bb2243c5aad...
Status: Image is up to date for python:3.13-slim

Image is up to date означает: digest в registry совпал с локальным, скачивать нечего. Обмен занял килобайты.

Узнать digest в registry, не загружая образ:

bash
docker manifest inspect python:3.13-slim -v \
    | python3 -c "
import json, sys
d = json.load(sys.stdin)
d = d if isinstance(d, list) else [d]
print('digest в registry:', d[0]['Descriptor']['digest'])
" 2>/dev/null || docker buildx imagetools inspect python:3.13-slim | head -3

Сравнение с локальным:

bash
docker image inspect python:3.13-slim --format 'локально: {{index .RepoDigests 0}}'

Закрепление в Dockerfile

bash
cd /tmp/tagdemo
DIGEST="$(docker image inspect python:3.13-slim \
          --format '{{index .RepoDigests 0}}' | cut -d@ -f2)"

cat > Dockerfile.pinned <<EOF
# Базовый образ закреплён по digest.
# Тег оставлен для читаемости; при расхождении приоритет у digest.
FROM python:3.13-slim@${DIGEST}

WORKDIR /app
CMD ["python", "-c", "print('закреплённая сборка')"]
EOF

cat Dockerfile.pinned
docker build -q -f Dockerfile.pinned -t pinned:1 . > /dev/null
docker run --rm pinned:1
text
# Базовый образ закреплён по digest.
# Тег оставлен для читаемости; при расхождении приоритет у digest.
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251

WORKDIR /app
CMD ["python", "-c", "print('закреплённая сборка')"]
закреплённая сборка

Теперь сборка воспроизводима: даже если мейнтейнеры перепривяжут тег 3.13-slim, ваша сборка возьмёт прежний образ.

Проверка приоритета digest над тегом:

bash
cat > Dockerfile.conflict <<EOF
FROM python:3.9-slim@${DIGEST}
CMD ["python", "--version"]
EOF
docker build -q -f Dockerfile.conflict -t conflict:1 . > /dev/null
docker run --rm conflict:1
text
Python 3.13.9

В теге написано 3.9, а получили 3.13 — digest победил. Это иллюстрирует, почему тег рядом с digest носит справочный характер и должен поддерживаться в актуальном состоянии вручную.

Digest в Compose

yaml
services:
  api:
    image: python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251

Просмотр всех платформ и их digest

bash
docker buildx imagetools inspect python:3.13-slim
text
Name:      docker.io/library/python:3.13-slim
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251

Manifests:
  Name:        docker.io/library/python:3.13-slim@sha256:a1b2c3d4e5f6...
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/amd64

  Name:        docker.io/library/python:3.13-slim@sha256:b2c3d4e5f6a7...
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm64/v8
  ...

Верхний Digest: — index digest, тот, что используется для закрепления. Ниже — manifest digest по платформам.

Уборка

bash
docker rm -f reg
docker rmi pinned:1 conflict:1 registry:2 2>/dev/null || true
docker images --filter 'reference=localhost:5000/*' -q | xargs -r docker rmi -f
rm -rf /tmp/tagdemo

Практическое упражнение

Задание. Напишите скрипт pin-check.sh <dockerfile>, который проверяет закрепление базовых образов в Dockerfile и помогает его выполнить.

Скрипт должен:

  1. Найти все инструкции FROM (учитывая multi-stage и AS).
  2. Для каждой определить: закреплён ли образ по digest, используется ли floating tag, отсутствует ли тег вовсе.
  3. Для незакреплённых образов получить актуальный digest из registry.
  4. Вывести готовые строки FROM с digest, которые можно скопировать в Dockerfile.
  5. Вернуть ненулевой exit code, если найден хотя бы один незакреплённый образ, — чтобы скрипт годился для CI.

Учтите: стадии multi-stage могут ссылаться друг на друга (FROM builder AS final) — такие ссылки закреплять не нужно.

Подсказки

Подсказка 1

Признак закрепления — наличие @sha256: в имени образа.

Подсказка 2

Имена стадий собираются из FROM ... AS <name>. Если очередной FROM ссылается на уже известное имя стадии, это внутренняя ссылка, а не внешний образ.

Подсказка 3

Получить digest, не загружая образ:

bash
docker buildx imagetools inspect <image> --format '{{println .Manifest.Digest}}'

Запасной вариант — загрузить и взять из RepoDigests.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# pin-check.sh — проверка закрепления базовых образов в Dockerfile.
set -uo pipefail

FILE="${1:?Использование: $0 <dockerfile>}"
[ -r "$FILE" ] || { echo "Файл не читается: $FILE" >&2; exit 2; }

declare -A STAGES=()
unpinned=0

get_digest() {
    local img="$1" d=""
    d="$(docker buildx imagetools inspect "$img" \
         --format '{{println .Manifest.Digest}}' 2>/dev/null | head -1)"
    if [ -z "$d" ]; then
        docker pull -q "$img" > /dev/null 2>&1 \
            && d="$(docker image inspect "$img" \
                    --format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}' \
                    2>/dev/null | cut -d@ -f2)"
    fi
    printf '%s' "$d"
}

echo "=== Проверка: $FILE ==="
echo

while read -r _ image rest; do
    [ -z "${image:-}" ] && continue

    # Имя стадии из "AS <name>"
    stage=""
    if [[ "$rest" =~ [Aa][Ss][[:space:]]+([A-Za-z0-9_.-]+) ]]; then
        stage="${BASH_REMATCH[1]}"
    fi

    # Ссылка на предыдущую стадию — не внешний образ
    if [ -n "${STAGES[$image]:-}" ]; then
        printf '  [—] %-42s ссылка на стадию, закрепление не требуется\n' "$image"
        [ -n "$stage" ] && STAGES["$stage"]=1
        continue
    fi
    [ -n "$stage" ] && STAGES["$stage"]=1

    # Уже закреплён
    if [[ "$image" == *"@sha256:"* ]]; then
        printf '  [+] %-42s закреплён\n' "${image%%@*}"
        continue
    fi

    unpinned=$((unpinned + 1))
    name="${image%%:*}"
    tag="${image##*:}"
    [ "$tag" = "$image" ] && tag="latest (не указан)"

    printf '  [!] %-42s НЕ закреплён (тег: %s)\n' "$image" "$tag"

    digest="$(get_digest "$image")"
    if [ -n "$digest" ]; then
        printf '      → FROM %s@%s\n' "$image" "$digest"
    else
        printf '      → digest получить не удалось (нет доступа к registry?)\n'
    fi
done < <(grep -iE '^[[:space:]]*FROM[[:space:]]' "$FILE")

echo
if [ "$unpinned" -eq 0 ]; then
    echo "Все внешние образы закреплены по digest."
    exit 0
else
    echo "Не закреплено образов: $unpinned"
    echo "Скопируйте предложенные строки FROM в $FILE."
    exit 1
fi

Проверка на multi-stage файле:

bash
mkdir -p /tmp/pintest && cd /tmp/pintest
cat > Dockerfile <<'EOF'
FROM python:3.13-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir build

FROM builder AS tester
RUN echo "тесты"

FROM python:3.13-slim@sha256:0000000000000000000000000000000000000000000000000000000000000000 AS runtime
COPY --from=builder /app /app
EOF

chmod +x pin-check.sh
./pin-check.sh Dockerfile; echo "exit code: $?"

Ожидаемый вывод:

text
=== Проверка: Dockerfile ===

  [!] python:3.13-slim                            НЕ закреплён (тег: 3.13-slim)
      → FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
  [—] builder                                    ссылка на стадию, закрепление не требуется
  [+] python:3.13-slim                            закреплён

Не закреплено образов: 1
Скопируйте предложенные строки FROM в Dockerfile.
exit code: 1

Разбор логики.

Скрипт различает три случая, и это главное в задании:

  • внешний образ без digest — требует закрепления;
  • внешний образ с digest — в порядке;
  • ссылка на стадию (FROM builder AS tester) — не образ, закреплять нечего.

Третий случай легко пропустить, и тогда скрипт будет пытаться загрузить из registry образ с именем builder.

Ненулевой exit code делает скрипт пригодным для CI: шаг проверки упадёт, если кто-то добавит незакреплённый базовый образ.

Как поддерживать закрепление в актуальном состоянии. Закрепив digest, вы перестаёте получать обновления безопасности базового образа автоматически. Это осознанный компромисс: обновление становится явным изменением в репозитории, которое проходит ревью и тесты. На практике обновление автоматизируют инструментами вроде Renovate или Dependabot, которые создают pull request с новым digest.

Проверка результата

bash
docker pull -q alpine:3.21
docker image inspect alpine:3.21 --format 'digest: {{index .RepoDigests 0}}'
docker image inspect alpine:3.21 --format 'image ID: {{.Id}}'

Убедитесь, что различаете эти два значения и можете объяснить, какое из них использовать для закрепления версии.

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

ОшибкаПричинаИсправление
Расчёт на неизменность тегаТег выглядит как версияТег — изменяемый указатель. Для гарантии использовать digest
docker run без обновления образаPull policy по умолчанию — missingЯвный docker pull или --pull=always
Закрепление по manifest digest вместо indexОба выглядят одинаковоИспользовать index digest из RepoDigests — он работает на всех платформах
Ожидание digest у локально собранного образаDigest появляется после взаимодействия с registryБрать digest из вывода docker push
Развёртывание по latestКажется, что это «текущая версия»Развёртывать по digest или по неизменяемому тегу
Тег и digest в FROM разошлисьОбновили digest, забыли тегОбновлять обе части; помнить, что приоритет у digest
Заполнение диска после серии docker pullСтарые образы теряют тег, но остаютсяПериодически удалять dangling images — урок 3.4
Закрепили digest и забыли про обновленияПатчи безопасности перестали приходитьАвтоматизировать обновление digest через Renovate или Dependabot

Контрольные вопросы

На понимание:

  1. Почему один и тот же тег в разное время может указывать на разные образы?
  2. Чем index digest отличается от manifest digest и какой использовать для закрепления?
  3. Почему docker run myapp:latest может запустить устаревший образ?
  4. Что происходит со старым образом, когда docker pull получает новую версию под тем же тегом?
  5. Почему у локально собранного образа поле RepoDigests пустое?

На применение:

  1. Как узнать digest образа в registry, не загружая его?
  2. Как закрепить базовый образ в Dockerfile так, чтобы строка оставалась читаемой?
  3. Как гарантировать, что в container запустится именно локально собранный образ?

На диагностику:

  1. Сборка одного и того же коммита вчера и сегодня дала образы разного размера. Что проверить в первую очередь?
  2. В FROM указан тег 3.9-slim, а python --version в container выводит 3.13.9. Как это возможно?

Краткое резюме

  1. Тег — изменяемый указатель «имя → digest», который владелец repository может перепривязать.
  2. Digest вычисляется из содержимого и неизменен: тот же digest означает те же байты.
  3. У multi-platform образа два digest: index (для всего образа) и manifest (для платформы).
  4. Для закрепления используется index digest — он работает на любой архитектуре.
  5. Pull policy по умолчанию — missing: при наличии образа локально registry не опрашивается.
  6. --pull=always заставляет проверить актуальность, --pull=never запрещает обращение.
  7. При обновлении тега старый образ остаётся на диске без тега и становится dangling.
  8. В FROM пишут и тег, и digest: тег для человека, digest для гарантии.
  9. При расхождении тега и digest приоритет у digest.
  10. Закрепление по digest отключает автоматические обновления — их нужно автоматизировать отдельно.

Официальные источники

ИсточникСсылкаЧто подтверждает
docker pull referencehttps://docs.docker.com/reference/cli/docker/image/pull/Загрузка по digest, поведение при уже загруженном образе, Image is up to date
docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/Флаг --pull и значения missing, always, never
docker images referencehttps://docs.docker.com/reference/cli/docker/image/ls/Флаг --digests, различие колонок DIGEST и IMAGE ID
docker buildx imagetools inspecthttps://docs.docker.com/reference/cli/docker/buildx/imagetools/inspect/Просмотр index и манифестов по платформам, digest каждого уровня
Dockerfile reference: FROMhttps://docs.docker.com/reference/dockerfile/#fromСинтаксис image:tag@digest, приоритет digest
Building best practiceshttps://docs.docker.com/build/building/best-practices/Рекомендация закреплять базовые образы по digest
Compose services: pull_policyhttps://docs.docker.com/reference/compose-file/services/Управление pull policy в Compose
OCI Image Specificationhttps://github.com/opencontainers/image-specОпределение digest, image index, привязка тегов

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Управление images
Главное оглавление

Markdown на GitHub ↗