Pada chapter sebelumnya kita sudah memegang peta tujuh belas instruksi Dockerfile, dan dua di antaranya bertugas khusus memindahkan berkas dari mesin kita ke dalam image: COPY dan ADD. Pekerjaan ini terdengar sepele — bukankah tinggal menyalin? — tetapi justru di sinilah keputusan-keputusan kecil menumpuk menjadi akibat besar: berkas mana yang ikut terkirim, kapan cache build batal, seberapa besar layer yang lahir, dan siapa pemilik berkas di dalam image. Budi akan merasakannya sendiri kali ini, karena folder myapp-nya mulai berisi berkas lain selain kode aplikasi. Chapter ini membedah cara kerja COPY beserta jebakannya, membandingkannya dengan ADD, lalu menutup dengan flag modern yang membuat COPY makin presisi.

Cara Kerja COPY #

Bentuk dasar COPY adalah COPY <sumber>... <tujuan>: satu atau beberapa sumber dari build context, lalu satu tujuan di dalam image. Tujuannya bisa berupa direktori kerja relatif atau path absolut, dan bila tujuan adalah folder, beri tanda garis miring di ujungnya agar maksudnya eksplisit:

dockerfile
COPY package.json server.js ./
COPY package.json server.js /app/
COPY konfigurasi/ /app/konfigurasi/

Mari kita telaah ketiga baris di atas:

  1. Banyak sumber sekaligus — package.json dan server.js disalin dalam satu instruksi sehingga menghasilkan satu layer; keduanya bisa juga dipisah dua instruksi COPY, tetapi hasilnya dua layer dan dua langkah build tanpa manfaat apa pun.
  2. ./ sebagai tujuan — titik berarti direktori kerja yang sedang aktif (ditentukan WORKDIR), sehingga hasilnya /app/package.json dan /app/server.js.
  3. Tujuan absolut dengan garis miring penutup — /app/ secara eksplisit menyatakan “ini folder”; COPY konfigurasi/ /app/konfigurasi/ menyalin isi folder konfigurasi ke folder bernama sama di dalam image.

Aturan penting yang melengkapi bentuk-bentuk di atas: bila sumbernya lebih dari satu, tujuan wajib berupa folder yang diakhiri garis miring. Pelanggaran aturan ini berakhir pada galat build:

text
ERROR: failed to solve: when using COPY with more than one source file, the destination must be a directory and end with a /

COPY Selalu Relatif ke Build Context #

Kesalahpahaman paling umum tentang COPY adalah mengira path sumbernya relatif terhadap lokasi berkas Dockerfile. Kenyataannya, path sumber selalu relatif terhadap build context — folder yang kita tunjuk sebagai argumen terakhir docker build. Budi membuktikannya dengan eksperimen kecil: ia keluar satu tingkat dari folder myapp lalu membangun dengan flag -f agar Dockerfile tetap ditemukan, sementara context-nya adalah folder induk:

bash
$ cd ..
$ docker build -f myapp/Dockerfile -t myapp:gagal .

Output yang Dihasilkan:

text
[+] Building 0.5s (9/9) ERROR
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 419B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.4s
 => [1/5] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.4s
 => CACHED [2/5] LABEL versi=1.1 deskripsi=API kecil milik Budi
 => CACHED [3/5] WORKDIR /app
 => ERROR [4/5] COPY package.json server.js ./                          0.0s
 > failed to compute cache key: "package.json" not found
---
 Dockerfile:13
--------------------
    12 |     # Berkas aplikasi disalin dari build context
    13 |     COPY package.json server.js ./
--------------------
ERROR: failed to solve: failed to compute cache key: "package.json" not found

Mari kita telaah kegagalan di atas:

  1. -f myapp/Dockerfile — flag -f membaca definisi build dari berkas yang bukan Dockerfile di akar context; berkasnya tetap ditemukan tanpa masalah.
  2. . sebagai context — titik kini menunjuk folder induk, sehingga COPY package.json server.js ./ mencari kedua berkas di akar folder induk — dan tidak menemukannya, karena package.json sebenarnya berada di myapp/package.json.
  3. Pesan galat inti — failed to compute cache key: "package.json" not found menandakan berkas sumber tidak ada di dalam context; BuildKit menghitung checksum sumber untuk keperluan cache, dan itulah tahap yang gagal duluan.
  4. Snippet Dockerfile:13 — Docker menunjuk baris ke-13 berkas Dockerfile sebagai sumber galat, lengkap dengan dua baris di sekitarnya, supaya kita langsung tahu instruksi mana yang harus diperbaiki.

Pelajarannya tegas: yang menentukan path COPY adalah context, bukan lokasi Dockerfile. Konsekuensi turunannya juga penting — berkas di luar context tidak mungkin disalin, karena builder memang hanya menerima isi context.

Bahaya COPY . . #

Bentuk yang paling sering terlihat di Dockerfile pemula adalah COPY . . — salin seluruh isi context ke direktori kerja. Untuk folder mungil seperti myapp, bentuk ini masih terasa jinak; untuk proyek nyata ia berbahaya karena semua ikut terkirim: node_modules ratusan megabyte, folder .git, berkas .env berisi kredensial, sampai berkas sementara yang kita lupa hapus. Dua biaya dibayar sekaligus: build context membengkak (semuanya dikirim ke builder lebih dulu), dan layer COPY ikut membengkak karena semuanya disalin ke image.

Demo: COPY Eksplisit versus COPY . . #

Budi kini mengalami sendiri beda perilaku cache dari dua bentuk tersebut. Setelah merapikan Dockerfile pada chapter sebelumnya, ia membangunnya sebagai myapp:1.1 — BuildKit mengeksekusi ulang LABEL beserta seluruh langkah setelahnya karena penyisipan instruksi menggeser rantai cache, sementara FROM tetap dilayani dari lokal. Kemudian ia menambahkan satu berkas catatan pengembangan ke dalam folder myapp:

bash
$ echo "catatan: belajar dockerfile hari ini" > catatan.txt

Budi membangun ulang tanpa mengubah Dockerfile sama sekali:

bash
$ docker build -t myapp:1.1 .

Output yang Dihasilkan:

text
[+] Building 0.8s (11/11) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 419B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.6s
 => [1/5] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.6s
 => CACHED [2/5] LABEL versi=1.1 deskripsi=API kecil milik Budi
 => CACHED [3/5] WORKDIR /app
 => CACHED [4/5] COPY package.json server.js ./
 => CACHED [5/5] EXPOSE 8000
 => exporting to image                                                  0.0s
 => => exporting layers                                                 0.0s
 => => writing image sha256:c63616cb491b19f1c63277fbbcbde6c2ab3d1ad4a837367ad4cb0b0e3836c763 0.1s
 => => naming to docker.io/library/myapp:1.1                            0.0s

Perhatikan hasilnya: seluruh langkah CACHED, termasuk COPY. Cache instruksi COPY dihitung dari checksum berkas-berkas yang disebut namanya — package.json dan server.js — dan keduanya tidak berubah sedikit pun, sehingga catatan.txt yang baru muncul sama sekali tidak terasa. Budi lalu penasaran dan mengganti baris salinnya menjadi COPY . ., lalu membangun sebagai image eksperimen:

bash
$ docker build -t myapp:eksperimen .

Output yang Dihasilkan:

text
[+] Building 0.9s (11/11) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 419B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.6s
 => [1/5] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.6s
 => CACHED [2/5] LABEL versi=1.1 deskripsi=API kecil milik Budi
 => CACHED [3/5] WORKDIR /app
 => [4/5] COPY . .                                                      0.1s
 => => transferring context: 1076B                                      0.0s
 => [5/5] EXPOSE 8000                                                   0.0s
 => exporting to image                                                  0.2s
 => => exporting layers                                                 0.1s
 => => writing image sha256:8630a9a8a9e63a7dcac5417095c4d87cbae8287ca804e7de81ee0b2a9c4bac1b 0.1s
 => => naming to docker.io/library/myapp:eksperimen                     0.0s

Mari kita telaah perbedaan dua output di atas:

  1. CACHED [4/5] COPY pada build pertama — instruksi eksplisit hanya menagih checksum dua berkas yang disebut namanya, sehingga berkas lain di context tidak memengaruhi cache-nya.
  2. [4/5] COPY . . dieksekusi ulang — checksum kini dihitung dari seluruh isi context, dan catatan.txt mengubahnya; satu berkas kecil cukup untuk membatalkan cache salinan.
  3. EXPOSE ikut dieksekusi ulang — perubahan pada satu instruksi membatalkan cache semua instruksi setelahnya dalam stage yang sama; aturan linear ini akan kita manfaatkan dan lawan berulang kali, dan mekanismenya dibedah tuntas pada Chapter Build Cache.
  4. transferring context: 1076B — context yang dikirim kini memuat empat berkas; pada proyek nyata, angka ini bisa melonjak ke ratusan megabyte hanya karena node_modules ikut serta.

Apakah catatan.txt benar-benar masuk ke dalam image eksperimen? Riwayat layer-nya menjawab:

bash
$ docker history myapp:eksperimen

Output yang Dihasilkan:

text
IMAGE          CREATED         CREATED BY                             SIZE    COMMENT
8630a9a8a9e6   30 seconds ago  CMD ["node" "server.js"]               0B
<missing>      30 seconds ago  EXPOSE 8000                            0B
<missing>      30 seconds ago  COPY . .                               657B
<missing>      30 seconds ago  WORKDIR /app                           0B
...

Baris COPY . . menyimpan 657B — 620B milik aplikasi ditambah 37B milik catatan.txt. Berkas yang tidak dibutuhkan aplikasi ternyata sudah menetap di dalam image, dan hanya docker image rm yang bisa mengeluarkannya. Budi mengembalikan baris Dockerfile-nya ke bentuk eksplisit, dan kondisi terakhir mesinnya kini terlihat begini:

bash
$ docker images myapp

Output yang Dihasilkan:

text
REPOSITORY        TAG          IMAGE ID       CREATED              SIZE
myapp             1.0          05303e530e6f   3 hours ago          130MB
myapp             1.1          c63616cb491b   15 minutes ago       130MB
myapp             eksperimen   8630a9a8a9e6   About a minute ago   130MB
node              22-alpine    4b2b8f1a9c7e   3 weeks ago          130MB

ADD: Menyalin Plus Ekstraksi Plus Unduhan #

Instruksi ADD adalah kakak tua dari COPY yang punya dua kemampuan tambahan: mengekstrak arsip tar lokal secara otomatis ke tujuan, dan mengunduh berkas dari URL. Tabel berikut membandingkan keduanya dalam lima aspek:

AspekCOPYADD
Menyalin berkas dan folder biasaYaYa
Mengekstrak arsip tar lokal otomatisTidakYa
Mengunduh berkas dari URLTidakYa
Niat penulis Dockerfile terbaca jelasYa — perilakunya eksplisitTidak — perilaku implisit dan mengejutkan
Kapan sebaiknya dipakaiHampir semua kasusHanya untuk arsip tar lokal

Kemampuan ekstraksi memang sah dipakai: ADD rootfs.tar.gz / mengekstrak isi arsip langsung ke root filesystem image — pola yang berguna untuk memuat root filesystem dari arsip. Jebakannya, keputusan “apa saja yang diekstrak” tidak terlihat dari baris instruksi, dan arsip yang tidak sengaja berada di path sumber akan dibongkar tanpa peringatan.

Flag Modern pada COPY #

Dua kebutuhan yang dulu menuntut trik RUN chown kini dilayani flag bawaan COPY, dan flag-flag ini memerlukan frontend Dockerfile modern — alasan persis kenapa parser directive # syntax=docker/dockerfile:1 pada chapter sebelumnya layak dibiasakan:

dockerfile
COPY --chown=appuser:appuser package.json server.js ./
COPY --chmod=755 entrypoint.sh /usr/local/bin/

Mari kita telaah kedua flag di atas:

  1. --chown=appuser:appuser — berkas tersalin langsung dimiliki user dan grup yang ditunjuk, tanpa layer tambahan; perannya penting saat image berencana berjalan sebagai non-root, materi yang kita bahas pada Chapter WORKDIR dan USER serta Chapter User dan Permissions di Section Konfigurasi Container.
  2. --chmod=755 — menetapkan izin berkas pada saat disalin, menggantikan RUN chmod yang menambah satu layer penuh hanya untuk mengubah satu angka.
  3. --link — flag ketiga yang lebih niche: salinan dibuat sebagai layer independen yang tidak menimpa layer di bawahnya, sehingga perubahan pada layer sebelumnya tidak membatalkan cache salinan ini; implikasi lengkapnya menyusul pada Chapter Build Cache.

Pola COPY yang Lazim #

Pengalaman Budi di atas dirangkum menjadi pola umum yang dipakai hampir semua Dockerfile aplikasi:

PolaContohAlasan
Manifest dependensi disalin lebih duluCOPY package.json go.mod Cargo.toml ./Cache instalasi dependensi bertahan saat kode berubah
Kode sumber disalin paling belakangCOPY src/ ./src/ setelah instalasiSuntingan kode hanya membangun ulang blok terakhir
Berkas konfigurasi dipisahkan sendiriCOPY nginx.conf /etc/nginx/nginx.confPerubahan konfigurasi tidak memicu build ulang dependensi
Salinan selektif, bukan seluruh folderCOPY src/ tests/ ./Context dan layer tetap ramping; berkas asing tak ikut

Pola manifest-dulu baru terasa dahsyat pada proyek berdependensi — dan myapp akan segera jadi seperti itu begitu Budi menambahkan pustaka. Namun sebelum itu, ada satu kebutuhan yang belum terjawab: image yang sama perlu berperilaku berbeda antara build dan runtime, antara versi satu dan versi dua. Dockerfile menjawabnya dengan sepasang instruksi yang sering tertukar — ENV dan ARG — dan itu materi chapter berikutnya.

Referensi #