Learning Platform
Глоссарий Troubleshooting
Урок 06.05 · 22 мин
Начальный
dockerpullpushlogintagregistry

Pull, push, search: ежедневная работа с registry

Все команды из этого урока — это твой ежедневный rotation Data Engineer’а. docker pull чтобы поднять Postgres локально, docker push чтобы залить свой ETL-образ в GHCR для деплоя, docker tag чтобы перепаковать image под другое имя registry, docker login чтобы authentic’нуться в private repo. Понимание того, что происходит на уровне OCI distribution API, помогает быстро дебажить странные ошибки авторизации и сетевые проблемы.

В этом уроке полный workflow private registry: build → tag → push → pull на другой машине → run.


Анатомия HTTP-запроса — что внутри request и response

docker pull: что происходит под капотом

Возьмём базовую команду:

docker pull postgres:16

Что делает Docker daemon пошагово:

  1. Resolve image reference. postgres:16 без registry-префикса → docker.io/library/postgres:16. Префикс library/ это namespace для official images.
  2. Аутентификация (если нужна). Для public images registry возвращает анонимный токен через GET /token?service=registry.docker.io&scope=repository:library/postgres:pull. Для private — нужен docker login заранее.
  3. GET манифеста. GET /v2/library/postgres/manifests/16 с Accept: application/vnd.oci.image.index.v1+json. Registry возвращает либо manifest list (multi-arch образ), либо одиночный manifest.
  4. Resolve manifest list → platform-specific manifest. Если manifest list — Docker выбирает запись с архитектурой, совпадающей с хостовой (linux/amd64 на Intel-Mac, linux/arm64 на M-серии, linux/amd64 в большинстве CI). Делает второй GET за конкретным manifest’ом.
  5. Параллельный pull слоёв. Manifest содержит список blobs (слоёв). Daemon тянет каждый по GET /v2/library/postgres/blobs/sha256:<digest> параллельно (по умолчанию 3 одновременных).
  6. Проверка digest. После каждого blob daemon считает SHA256 содержимого. Если не совпадает с заявленным — ошибка.
  7. Unpack. Tar.gz распаковывается в /var/lib/docker/overlay2/<layer-id>/diff/. Если такой layer уже есть — пропускается (“Already exists”).
  8. Update local cache. В /var/lib/docker/image/overlay2/repositories.json добавляется запись postgres:16 → sha256:<image-id>.
$ docker pull postgres:16
16: Pulling from library/postgres
4f4fb700ef54: Already exists      # пустой слой (метадата), уже был
9aa3a5f5765f: Pull complete       # base debian rootfs
3e7c1f8e2a4d: Pull complete       # apt deps
b1c8d4e7a2c3: Pull complete       # postgres binaries
Digest: sha256:5d3e8b7f0a2c1e4b...
Status: Downloaded newer image for postgres:16
docker.io/library/postgres:16

Status “Image is up to date” означает: локально уже есть образ с тем же digest’ом, ничего тянуть не надо.


docker push: симметрично pull

docker push это обратный процесс, с одной важной деталью: registry проверяет, существует ли blob (по digest’у) — если да, daemon его не отправляет повторно. Это layer-level дедупликация.

Полный workflow push в private GHCR:

# 1. Login (один раз, токен сохраняется в ~/.docker/config.json)
echo $GITHUB_PAT | docker login ghcr.io -u myuser --password-stdin
# Login Succeeded

# 2. Соберём образ
docker build -t myapp:v1 .

# 3. Tag для GHCR (image name должен начинаться с registry host)
docker tag myapp:v1 ghcr.io/myorg/myapp:v1

# 4. Push
docker push ghcr.io/myorg/myapp:v1
# The push refers to repository [ghcr.io/myorg/myapp]
# 9f3d4c2b1a0e: Layer already exists      # этот слой уже был в registry
# 5e8a7b6c4d3f: Pushed                    # новый слой, загружен
# 1c2d3e4f5a6b: Layer already exists
# v1: digest: sha256:abc123... size: 1234

Под капотом push идёт через chunked upload:

  1. POST /v2/myorg/myapp/blobs/uploads/ — начать upload, registry возвращает 202 с Location.
  2. PATCH <location> — chunks of blob.
  3. PUT <location>?digest=sha256:... — финализировать с заявленным digest.
  4. PUT /v2/myorg/myapp/manifests/v1 — register manifest под тегом v1.
docker push: chunked upload и дедупликация слоёв
Слои, уже существующие в registry, не пересылаются повторно
Docker DaemonDocker daemon: для каждого слоя проверяет HEAD /v2/repo/blobs/{digest}. Если 200 -- слой есть, не push. Если 404 -- push.
HEAD blob (check)
RegistryRegistry хранит blobs по digest. Дедупликация: один и тот же слой в разных образах -- одна копия в storage.
Docker DaemonDaemon: push только новых слоёв. Старые base layers (debian, alpine, python runtime) уже есть -- не пересылаются.
PATCH chunks (new blob)
RegistryRegistry собирает blob из chunks, проверяет digest, сохраняет.
Docker DaemonDaemon: финальный PUT manifest -- регистрирует tag в registry.
PUT manifest (tag)
RegistryRegistry: tag v1 теперь указывает на новый manifest. Image доступен для pull.

docker tag: переименование без копирования

docker tag создаёт новое имя для существующего image ID. Никаких слоёв не копируется — это просто новая запись в локальном кэше.

# Образ собран с локальным именем
docker build -t myapp:dev .
# myapp:dev now points to sha256:abc123...

# Добавляем второй tag для того же image ID
docker tag myapp:dev ghcr.io/myorg/myapp:v1.2.3

# Третий tag (для latest)
docker tag myapp:dev ghcr.io/myorg/myapp:latest

# Все три имени указывают на ОДИН И ТОТ ЖЕ образ
docker images | grep myapp
# REPOSITORY              TAG       IMAGE ID       SIZE
# myapp                   dev       sha256:abc...  142MB
# ghcr.io/myorg/myapp     v1.2.3    sha256:abc...  142MB
# ghcr.io/myorg/myapp     latest    sha256:abc...  142MB

# Push -- registry получит образ один раз, regstr два tag-pointer'а
docker push ghcr.io/myorg/myapp:v1.2.3
docker push ghcr.io/myorg/myapp:latest
# Второй push быстрый -- все слои уже в registry, регистрируется только новый tag
NOTE

Tag это локальный alias. Если в registry образ перезалит другим разработчиком, твой локальный myapp:dev будет указывать на старый digest. Чтобы синхронизироваться: docker pull myapp:dev (если тег публичный) или docker rmi myapp:dev && docker pull ....


docker login и хранение credentials

docker login сохраняет credentials в ~/.docker/config.json:

{
  "auths": {
    "ghcr.io": {
      "auth": "bXl1c2VyOmdocF8xMjM0NTY3ODkw"
    }
  }
}

Где auth это base64(username:password). Это plaintext (base64 — это encoding, не шифрование). Не лучшая практика для credentials.

Более безопасные варианты:

1. credsStore. Использовать system keychain (macOS Keychain, GNOME Keyring, Windows Credential Manager). В config.json:

{
  "credsStore": "osxkeychain"   // или "secretservice" на Linux
}

2. credHelpers. Per-registry helper. Например, ecr-login для AWS:

{
  "credHelpers": {
    "123456789012.dkr.ecr.us-east-1.amazonaws.com": "ecr-login"
  }
}

При pull/push Docker автоматически вызывает helper для получения свежего токена (IAM-based, истекает через 12 часов).

3. Short-lived tokens в CI. В GitHub Actions: password: ${{ secrets.GITHUB_TOKEN }} (auto-expiring per-job).

Logout: docker logout ghcr.io — удаляет запись из config.json.

# Workflow для GHCR с PAT
echo $GITHUB_PAT | docker login ghcr.io -u myuser --password-stdin
# ... работаешь с registry
docker logout ghcr.io

docker search: поиск образов в Docker Hub

docker search ищет ТОЛЬКО в Docker Hub (других registries не поддерживается):

docker search --limit 5 postgres
# NAME                DESCRIPTION                          STARS      OFFICIAL
# postgres            The PostgreSQL object-relational...  14523      [OK]
# bitnami/postgresql  Bitnami PostgreSQL Docker Image      234
# postgis/postgis     PostGIS spatial extension            845
# timescale/timescaledb  TimescaleDB...                    412
# crunchydata/crunchy-postgres  PostgreSQL container        67

Колонка OFFICIAL = [OK] означает это official image от Docker Inc / upstream maintainers (postgres, redis, python, node и т.д.). Им можно доверять как минимум на уровне “не вредоносное”.

Для GHCR, ECR, Harbor поиск идёт через их UI (нет CLI-эквивалента docker search). Это исторический legacy команды.


Полный workflow private registry

Соберём всё вместе. Сценарий: ты собрал ETL-образ локально, хочешь залить в GHCR, а коллега должен запустить его на своей машине.

# ── На твоей машине ──

# 1. Build
cat > Dockerfile <<EOF
FROM python:3.13-slim
WORKDIR /app
COPY etl.py requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "etl.py"]
EOF

docker build -t my-etl:v1 .

# 2. Login в GHCR (с Personal Access Token)
echo $GITHUB_PAT | docker login ghcr.io -u myuser --password-stdin

# 3. Tag для GHCR
docker tag my-etl:v1 ghcr.io/myorg/my-etl:v1

# 4. Push
docker push ghcr.io/myorg/my-etl:v1

# Получи digest для коллеги
docker inspect ghcr.io/myorg/my-etl:v1 --format '{{index .RepoDigests 0}}'
# ghcr.io/myorg/my-etl@sha256:abc123...

# ── На машине коллеги ──

# 1. Login (если image private)
echo $GITHUB_PAT_COLLEAGUE | docker login ghcr.io -u colleague --password-stdin

# 2. Pull через digest (гарантированно тот же образ)
docker pull ghcr.io/myorg/my-etl@sha256:abc123...

# 3. Run
docker run --rm ghcr.io/myorg/my-etl@sha256:abc123...
# Запустит python etl.py внутри контейнера
TIP

В DE-CI обычно build, tag, push выполняются за один шаг GitHub Actions / GitLab CI с buildx и кэшем. Локальный flow выше — для понимания, что происходит под капотом. В реальности CI ещё навешивает security scan (Trivy), signing (Cosign) и метаданные (SBOM).


docker pull для private images: типовые ошибки

unauthorized: authentication required — нет credentials для этого registry. docker login <registry> или установить credHelper.

denied: access forbidden — credentials есть, но нет прав на pull/push в этот repo. Проверить scope токена (для GHCR — read:packages / write:packages).

no matching manifest for linux/amd64 in the manifest list entries — образ собран только под другую архитектуру (часто linux/arm64, если собирали на M-Mac без cross-platform flags). Решение: при build использовать docker buildx build --platform linux/amd64,linux/arm64 ....

x509: certificate signed by unknown authority — pull с private registry на самоподписанном HTTPS. Решение: добавить CA в системные trust certs, или (для dev) добавить в /etc/docker/daemon.json:

{ "insecure-registries": ["harbor.dev.local:5000"] }

И перезапустить docker daemon. В проде insecure-registries НЕЛЬЗЯ.


Попробуй сам

Полный цикл с локальным registry:

# Запусти локальный registry на :5000
docker run -d -p 5000:5000 --name local-reg registry:2

# Pull маленького образа
docker pull alpine:3.21

# Tag для локального registry
docker tag alpine:3.21 localhost:5000/myalpine:v1

# Push
docker push localhost:5000/myalpine:v1
# Слои уже есть в local cache, но в registry их нет -- pushes everything

# Список тегов в registry через API
curl http://localhost:5000/v2/myalpine/tags/list
# {"name":"myalpine","tags":["v1"]}

# Удалить локальный alpine
docker rmi alpine:3.21 localhost:5000/myalpine:v1

# Pull обратно из локального registry
docker pull localhost:5000/myalpine:v1

# Cleanup
docker rm -f local-reg
docker rmi localhost:5000/myalpine:v1

Проверка знанийKnowledge check
Команда собрала на macOS M2 (arm64) образ data-pipeline:v1 и push'нула в GHCR. На production-машине (linux/amd64) docker pull проходит, но docker run падает с 'exec format error'. В чём причина и как исправить?
ОтветAnswer
Образ был собран только под arm64 (хост-архитектуру MacBook). При запуске на amd64 ядро не может выполнить ELF-бинарник arm64 -- exec format error. Исправление: пересобрать образ под обе платформы через docker buildx, который умеет cross-platform build. Команда: docker buildx create --use; docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/org/data-pipeline:v1 --push . buildx создаст multi-arch manifest list, при pull на amd64 будет выбран amd64-вариант автоматически.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что делает команда docker tag myapp:v1 ghcr.io/myorg/myapp:v1?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5