Belajar Docker - Docker Registry & Image Distribution
Episode 12 of 28

Belajar Docker - Docker Registry & Image Distribution

Memahami bagaimana image didistribusikan: tata nama registry, docker login, tag, push, pull, digest immutable untuk pinning, perbandingan Docker Hub dan GHCR, hingga membangun private registry:2 sendiri dengan TLS dan basic auth beserta jebakan rate limit dan credentials di CI.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

Setelah di episode 11 sebelumnya kita membuat Compose bisa beradaptasi antar environment, pada episode kali ini kita menjawab pertanyaan yang selama ini kita lalui begitu saja: dari mana image-image itu berasal, dan bagaimana cara mendistribusikannya? Sejak episode 0 kita mengetik docker run nginx atau docker pull postgres:16-alpine tanpa pernah mempertanyakan mekanisme di baliknya. Registry — tempat image disimpan dan dibagikan — adalah bagian dari arsitektur Docker yang paling sering dianggap remeh, padahal di dunia nyata ia adalah "pipa distribusi" dari seluruh software.

Pahami ini dengan baik karena konsekuensinya nyata: tata nama image yang salah berarti image jatuh ke tempat yang tidak kalian duga; lupa autentikasi berarti push yang gagal misterius; tag yang asal-asalan berarti tim tidak bisa membedakan versi mana yang sebenarnya dipakai produksi; dan kredensial yang bocor di CI bisa berarti seluruh private image kalian terekspos. Di perusahaan, insiden keamanan supply chain sering berawal di registry — bukan di kode.

Di episode ini kita akan membedah anatomi nama image, menguasai docker login, tag, push, dan pull, memahami digest sebagai identitas immutable dan cara pinning, membandingkan Docker Hub dengan GitHub Container Registry, membangun private registry sendiri berbasis registry:2 dengan TLS dan basic auth, serta menutup dengan pitfalls yang paling sering menjegal: rate limit, latest yang mutable, dan kredensial yang bocor di CI.

Pembahasan Utama

Mengapa Registry Ada

Registry adalah layanan yang menyimpan dan mendistribusikan Docker images. Bayangkan tanpa registry: setiap kali ingin menjalankan aplikasi, kalian harus mengirim file image secara manual dari satu mesin ke mesin lain — tidak praktis, tidak aman, dan tidak bisa di-versioning. Registry memecahkan tiga masalah sekaligus:

  1. Penyimpanan terpusat — satu tempat untuk menyimpan semua versi image, bisa di-pull dari mana saja.
  2. Distribusi efisien — hanya layer yang belum dimiliki mesin yang diunduh; layer yang sama dipakai bersama antar image (ingat arsitektur layer di episode 2 dan 23).
  3. Kontrol akses — registry bisa publik (semua orang) atau privat (hanya yang punya kredensial).

Analoginya: registry adalah remote Git bagi image. docker pull itu git clone, docker push itu git push, dan docker tag itu memberi nama pada sebuah commit — dalam hal ini, pada satu snapshot image.

Anatomi Nama Image: registry/namespace/repo:tag

Setiap image punya nama lengkap dengan struktur tertentu. Bentuk lengkapnya:

Struktur nama image
[registry]/[namespace]/[repository]:[tag]
ghcr.io/arman-dwi-pangestu/my-app:1.0.0

Ketika sebagian bagian dihilangkan, Docker mengisi default-nya. Mari kita bedah:

ContohRegistryNamespaceRepoTag
nginxdocker.iolibrarynginxlatest
postgres:16-alpinedocker.iolibrarypostgres16-alpine
ghcr.io/arman-dwi-pangestu/my-app:1.0.0ghcr.ioarman-dwi-pangestumy-app1.0.0
localhost:5000/team/app:v2localhost:5000teamappv2
  • Registry default adalah docker.io (Docker Hub). Menulis docker pull nginx sebenarnya berarti docker pull docker.io/library/nginx:latest.
  • library adalah namespace khusus untuk image official (nginx, postgres, redis, dan seterusnya). Image non-official di Docker Hub memakai nama user/org sebagai namespace, misal docker pull traefik/traefik berarti namespace traefik.
  • Tag default adalah latest. Ini perlu diwaspadai — kita bahas di pitfalls.

Satu kebiasaan yang sangat disarankan: selalu tulis tag secara eksplisit, dan untuk produksi, tulis nama lengkap termasuk registry-nya. Nama image adalah alamat; alamat yang samar menghasilkan pull yang tidak terduga.

docker login: Autentikasi ke Registry

Registry privat dan push ke registry mana pun mengharuskan autentikasi. Perintahnya selalu berbentuk docker login <host>:

Login ke Docker Hub dan GHCR
docker login
docker login ghcr.io -u arman-dwi-pangestu
docker logout ghcr.io
  • docker login tanpa argumen berarti Docker Hub — kalian diminta username dan password/token.
  • Untuk Docker Hub, praktik terbaiknya bukan password akun, melainkan access token yang dibuat di halaman pengaturan akun, dengan scope terbatas (misal Read & Write saja untuk repo tertentu).
  • Untuk GHCR, yang dipakai adalah Personal Access Token (PAT) dengan scope read:packages / write:packages, atau GITHUB_TOKEN di CI.
  • docker logout membersihkan kredensial lokal — jangan biarkan login mengendap di mesin bersama.

Warning

Di Linux, kredensial dari docker login disimpan (ter-base64, tidak terenkripsi) di ~/.docker/config.json. Jika mesinnya dipakai bersama, konfigurasikan credential helper (misal docker-credential-pass atau integrasi dengan keychain/secret service) agar kredensial tidak teronggok sebagai teks yang mudah dibaca. Jangan pernah membiarkan config.json ikut ter-commit ke repository.

docker tag dan docker push: Mempublikasikan Image

docker tag tidak menyalin image — ia membuat nama/tanda lain untuk image yang sama (ingat analogi commit: tag adalah label pada snapshot yang sama). Kemudian docker push mengunggah layer-layer image ke registry:

Tag dan push ke GHCR
docker tag my-app:1.0.0 ghcr.io/arman-dwi-pangestu/my-app:1.0.0
docker push ghcr.io/arman-dwi-pangestu/my-app:1.0.0
Output push (diringkas)
The push refers to repository [ghcr.io/arman-dwi-pangestu/my-app]
8cbe492...: Pushed
6e2f2ad...: Pushed
2c6e27a...: Layer already exists
1.0.0: digest: sha256:0b5e4a6c3d9f... size: 1734

Perhatikan baris "Layer already exists" — ini bukti efisiensi registry: layer yang sudah ada di server tidak diunggah ulang. docker pull bekerja sebaliknya: ia hanya mengambil layer yang belum dimiliki mesin lokal. Inilah mengapa image yang sama di banyak server tidak berarti disk penuh terulang — layer dibagi bersama.

Digest: Identitas Immutable dan Pinning

Di akhir push, Docker menampilkan digest: sha256:.... Apa itu? Digest adalah hash SHA-256 dari manifest image — identitas kriptografis yang immutable. Artinya: digest yang sama selalu merujuk ke konten yang sama persis, tidak peduli tag-nya.

  • Tag bersifat mutablemy-app:latest bisa menunjuk ke image berbeda setiap kali di-push.
  • Digest bersifat immutablemy-app@sha256:0b5e4a6c... selamanya menunjuk ke konten yang sama.

Cara membaca digest sebuah image:

Melihat digest image
docker images --digests ghcr.io/arman-dwi-pangestu/my-app
docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/arman-dwi-pangestu/my-app:1.0.0

Untuk deployment yang benar-benar reproducible, pin image ke digest:

Pinning image ke digest di compose.yaml
services:
  api:
    image: ghcr.io/arman-dwi-pangestu/my-app@sha256:0b5e4a6c3d9f...e3f

Dengan pinning, docker compose pull selalu mengambil konten yang sama persis — tidak peduli apa pun yang terjadi pada tag latest. Trade-off-nya: kalian harus memperbarui digest secara sadar setiap kali rilis baru (dan ini justru bagus — tidak ada perubahan yang "tak terlihat"). Pilihan yang seimbang: gunakan tag versi (1.0.0) untuk kenyamanan, dan digest untuk deployment kritis atau saat tag sedang dipindahkan.

Docker Hub vs GitHub Container Registry

Dua registry publik yang paling sering dipakai memiliki karakter berbeda. Pilihannya memengaruhi workflow kalian:

AspekDocker HubGHCR (ghcr.io)
Akses defaultPublik; repo privat berbayarPublik/private mengikuti visibility repo GitHub
Image officialYa (ecosystem terbesar)Tidak (hanya user/org)
Autentikasi CIAccess token, butuh setupGITHUB_TOKEN otomatis di Actions
Pull rate limit (anonym)~100/6 jam per IPTidak ada limit praktis untuk publik
Cocok untukImage publik & komunitasImage privat + tim yang sudah di GitHub

Rate limit Docker Hub adalah alasan paling umum pull gagal secara tiba-tiba: pengguna anonim dibatasi (sekitar 100 pull per 6 jam per IP), dan yang terautentikasi mendapat kuota lebih besar. Di CI dengan runner bersama, limit ini cepat tersentuh. Solusinya: login (docker login) sebelum pull di CI, atau pindahkan image privat ke registry yang tidak menerapkan limit agresif seperti GHCR.

Aturan praktis yang masuk akal: publik → Docker Hub, privat → GHCR. Jika kode kalian sudah di GitHub, GHCR menghilangkan biaya dan setup tambahan — tokennya sudah tersedia di Actions, dan visibility image mengikuti repository.

Private Registry: Dari localhost:5000 sampai TLS + Basic Auth

Tidak semua image boleh menginjak registry publik. Untuk image internal — kode yang belum boleh publik — kalian butuh private registry. Docker menyediakan image resmi registry:2 yang bisa dijalankan dalam hitungan menit.

Langkah 0 — Uji coba lokal (tanpa TLS):

Private registry uji coba di localhost
docker run -d -p 5000:5000 --name registry registry:2
docker tag my-app:1.0.0 localhost:5000/my-app:1.0.0
docker push localhost:5000/my-app:1.0.0
docker pull localhost:5000/my-app:1.0.0

localhost adalah kasus khusus yang diizinkan Docker tanpa TLS. Begitu keluar dari localhost, dua hal wajib: TLS (agar lalu lintas terenkripsi dan daemon mau menggunakannya) dan autentikasi (agar tidak sembarang orang bisa push).

Langkah 1 — Siapkan kredensial basic auth dengan htpasswd:

Buat file htpasswd memakai image registry:2
mkdir -p registry/{auth,certs}
docker run --rm --entrypoint htpasswd registry:2 \
  -Bbn arman 's3cr3t-kuat' > registry/auth/htpasswd

-B memakai bcrypt (wajib untuk registry:2), -b memberi password via argumen, -n mencetak ke stdout. File htpasswd ini berisi username dan hash password.

Langkah 2 — Siapkan sertifikat TLS:

Buat self-signed certificate
openssl req -newkey rsa:2048 -nodes -keyout registry/certs/domain.key \
  -x509 -days 365 -out registry/certs/domain.crt \
  -subj "/CN=registry.example.com"

Tip

Untuk produksi sungguhan, jangan pakai self-signed — gunakan sertifikat dari CA publik (misal Let's Encrypt). Self-signed memaksa kalian menyalin domain.crt dan mempercayainya di setiap mesin klien, yang mudah sekali terlewat. Nama di CN/SAN juga harus cocok dengan hostname yang dipakai di nama image (registry.example.com:5000/...) — jika tidak, verifikasi TLS gagal.

Langkah 3 — Jalankan registry dengan TLS + auth:

Registry produksi dengan TLS dan basic auth
docker run -d -p 5000:5000 --name registry \
  -v "$PWD/registry/auth":/auth \
  -v "$PWD/registry/certs":/certs \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  -e REGISTRY_HTTP_SECRET=random-string-panjang \
  registry:2

Variabel REGISTRY_AUTH* mengaktifkan basic auth, REGISTRY_HTTP_TLS* menunjuk ke sertifikat, dan REGISTRY_HTTP_SECRET adalah kunci untuk menandatangani token internal — jangan pakai nilai yang bisa ditebak.

Langkah 4 — Percayai sertifikat di klien, lalu login dan push:

LinuxSertakan CA di daemon klien
sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000
sudo cp registry/certs/domain.crt /etc/docker/certs.d/registry.example.com:5000/ca.crt
sudo systemctl restart docker
Login dan push ke registry internal
docker login registry.example.com:5000 -u arman
docker tag my-app:1.0.0 registry.example.com:5000/my-app:1.0.0
docker push registry.example.com:5000/my-app:1.0.0

Struktur direktori /etc/docker/certs.d/<host>:<port>/ca.crt adalah mekanisme resmi Docker untuk mempercayai CA spesifik per registry. Jangan menggantinya dengan --insecure-registry — itu mematikan verifikasi TLS sepenuhnya dan membuka jalan untuk serangan man-in-the-middle.

Caution

registry:2 yang berjalan sebagai kontainer menyimpan datanya di dalam layer kontainer. Begitu kontainer dihapus (atau di-docker run ulang dengan nama yang sama), seluruh image privat kalian lenyap. Selalu pasang named volume (atau bind mount) ke /var/lib/registry — persis pelajaran episode 8. Registry tanpa persistent storage adalah registry yang kehilangan data pada maintenance pertama.

Credentials di CI: Jangan Pernah Masuk ke Image

Kesalahan paling mahal di seluruh episode ini: kredensial registry bocor ke dalam image. Pola yang keliru:

Pola SALAH: kredensial masuk layer image
FROM alpine:3.20
RUN docker login -u $CI_USER -p $CI_PASS ghcr.io \
    && docker pull ghcr.io/private/helper \
    && docker login --logout ghcr.io

Masalahnya: perintah RUN menulis ke layer baru yang permanen — baris docker login -u $CI_USER -p $CI_PASS ter-rekam dalam riwayat image (bisa dilihat dengan docker history) dan kredensial bisa bocor ke siapa pun yang punya akses image. Pola yang benar:

  • Jangan pernah menjalankan docker login di dalam Dockerfile. Authentikasi ke registry adalah urusan mesin yang membangun, bukan urusan image.
  • Gunakan build secrets (episode 18) jika memang harus menarik resource privat saat build, atau — lebih baik — pull image privat sebelum build di host/CI, lalu gunakan secara lokal.
  • Di GitHub Actions, GHCR bisa diakses dengan GITHUB_TOKEN bawaan — tidak ada string rahasia yang perlu di-copy ke mana pun:
Login GHCR di GitHub Actions
- name: Login ke GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

Prinsipnya sederhana: kredensial tidak pernah masuk ke image; kredensial hanya hidup di environment yang memakainya. Jika sebuah image bisa menampilkan secret dari dalam kontainer, desainnya salah.

Pitfalls yang Paling Sering Menjegal

  1. Rate limit Docker Hub. Pull anonim dibatasi ~100 kali per 6 jam per IP. Di CI atau server bersama, ini sering menjadi "gagal pull misterius". Login sebelum pull, atau pindahkan image ke registry tanpa limit agresif.

  2. Tag latest yang mutable. latest tidak memberi tahu apa pun: ia bisa menunjuk ke versi berbeda kapan saja dan merusak reprodusibilitas. Selalu tag dengan versi semantik (1.0.0), dan untuk deployment kritis, pin ke digest.

  3. Kredensial di image / di CI file. docker login di Dockerfile, token di compose.yaml, atau .env yang ter-commit adalah jalur kebocoran. Kredensial hidup di secret store, bukan di file yang dibagikan.

  4. Registry self-signed tanpa trust di klien. Push ke registry yang memakai self-signed cert gagal dengan x509: certificate signed by unknown authority karena CA belum dipercaya. Salin CA ke /etc/docker/certs.d/<host>:<port>/ca.crt di setiap klien — atau gunakan CA publik.

  5. Nama host tidak cocok dengan CN sertifikat. Sertifikat untuk registry.example.com tidak valid untuk registry atau IP. Nama di docker login dan di tag image harus persis sesuai CN/SAN.

  6. Registry kontainer tanpa persistent volume. registry:2 yang menyimpan data di layer kontainer kehilangan semua image saat kontainer dihapus. Pasang volume ke /var/lib/registry.

  7. Menimpa tag yang sama terus-menerus. docker push ...:1.0.0 dua kali berarti 1.0.0 menunjuk ke konten baru — riwayat lama hilang dari tag itu. Untuk image yang harus tidak bisa berubah, gunakan tag immutable (misal 1.0.0-<build> atau digest) dan aktifkan fitur tag protection di registry.

Penutup

Pada episode 12 ini kita telah memahami jalur distribusi image: registry sebagai tempat penyimpanan dan distribusi yang efisien (layer yang sama tidak diunduh ulang), anatomi nama image (registry/namespace/repo:tag dengan docker.io dan library sebagai default), alur autentikasi dan publikasi (docker login, tag, push, pull), digest sha256 sebagai identitas immutable untuk pinning yang reproducible, perbandingan Docker Hub vs GHCR beserta rate limit-nya, membangun private registry berbasis registry:2 dengan TLS dan basic auth (htpasswd, openssl, certs.d), serta menghindari kebocoran kredensial di CI.

Inti yang harus kalian bawa:

  • Nama image adalah alamat — tulis lengkap, tulis tag, dan untuk produksi pin ke digest.
  • docker tag hanya membuat label; layer dikirim hanya jika belum ada.
  • Digest immutable, tag mutable — gunakan keduanya secara sadar.
  • Publik → Docker Hub, privat → GHCR, internal → private registry registry:2.
  • Private registry wajib TLS + auth; CA di certs.d, bukan --insecure-registry.
  • Kredensial tidak pernah masuk ke image; di CI gunakan secret built-in (misal GITHUB_TOKEN).

Sekarang image kalian tersimpan dan terdistribusi dengan rapi. Tapi ada pertanyaan yang semakin mendesak seiring image makin mudah didistribusikan: apakah image yang kalian tarik dari registry itu aman untuk dijalankan? Registry bisa saja berisi image berbahaya — dan bahkan image yang kalian bangun sendiri bisa membawa base image yang rentan. Di episode 13 selanjutnya kita masuk ke ranah Container Security & Hardening Best Practices: risiko menjalankan kontainer sebagai root, membatasi capabilities, read-only filesystem, resource limits, seccomp/AppArmor, hingga Rootless Docker. Sampai jumpa di episode 13!

Belajar Docker - Docker Registry & Image Distribution | Belajar Docker