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 пошагово:
- Resolve image reference.
postgres:16без registry-префикса →docker.io/library/postgres:16. Префиксlibrary/это namespace для official images. - Аутентификация (если нужна). Для public images registry возвращает анонимный токен через
GET /token?service=registry.docker.io&scope=repository:library/postgres:pull. Для private — нуженdocker loginзаранее. - GET манифеста.
GET /v2/library/postgres/manifests/16сAccept: application/vnd.oci.image.index.v1+json. Registry возвращает либо manifest list (multi-arch образ), либо одиночный manifest. - Resolve manifest list → platform-specific manifest. Если manifest list — Docker выбирает запись с архитектурой, совпадающей с хостовой (linux/amd64 на Intel-Mac, linux/arm64 на M-серии, linux/amd64 в большинстве CI). Делает второй GET за конкретным manifest’ом.
- Параллельный pull слоёв. Manifest содержит список blobs (слоёв). Daemon тянет каждый по
GET /v2/library/postgres/blobs/sha256:<digest>параллельно (по умолчанию 3 одновременных). - Проверка digest. После каждого blob daemon считает SHA256 содержимого. Если не совпадает с заявленным — ошибка.
- Unpack. Tar.gz распаковывается в
/var/lib/docker/overlay2/<layer-id>/diff/. Если такой layer уже есть — пропускается (“Already exists”). - 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:
POST /v2/myorg/myapp/blobs/uploads/— начать upload, registry возвращает 202 с Location.PATCH <location>— chunks of blob.PUT <location>?digest=sha256:...— финализировать с заявленным digest.PUT /v2/myorg/myapp/manifests/v1— register manifest под тегом v1.
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
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 внутри контейнера
В 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