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:
COPY package.json server.js ./
COPY package.json server.js /app/
COPY konfigurasi/ /app/konfigurasi/Mari kita telaah ketiga baris di atas:
- Banyak sumber sekaligus —
package.jsondanserver.jsdisalin dalam satu instruksi sehingga menghasilkan satu layer; keduanya bisa juga dipisah dua instruksiCOPY, tetapi hasilnya dua layer dan dua langkah build tanpa manfaat apa pun. ./sebagai tujuan — titik berarti direktori kerja yang sedang aktif (ditentukanWORKDIR), sehingga hasilnya/app/package.jsondan/app/server.js.- Tujuan absolut dengan garis miring penutup —
/app/secara eksplisit menyatakan “ini folder”;COPY konfigurasi/ /app/konfigurasi/menyalin isi folderkonfigurasike 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:
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:
$ cd ..
$ docker build -f myapp/Dockerfile -t myapp:gagal .Output yang Dihasilkan:
[+] 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 foundMari kita telaah kegagalan di atas:
-f myapp/Dockerfile— flag-fmembaca definisi build dari berkas yang bukanDockerfiledi akar context; berkasnya tetap ditemukan tanpa masalah..sebagai context — titik kini menunjuk folder induk, sehinggaCOPY package.json server.js ./mencari kedua berkas di akar folder induk — dan tidak menemukannya, karenapackage.jsonsebenarnya berada dimyapp/package.json.- Pesan galat inti —
failed to compute cache key: "package.json" not foundmenandakan berkas sumber tidak ada di dalam context; BuildKit menghitung checksum sumber untuk keperluan cache, dan itulah tahap yang gagal duluan. - 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:
$ echo "catatan: belajar dockerfile hari ini" > catatan.txtBudi membangun ulang tanpa mengubah Dockerfile sama sekali:
$ docker build -t myapp:1.1 .Output yang Dihasilkan:
[+] 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.0sPerhatikan 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:
$ docker build -t myapp:eksperimen .Output yang Dihasilkan:
[+] 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.0sMari kita telaah perbedaan dua output di atas:
CACHED [4/5] COPYpada build pertama — instruksi eksplisit hanya menagih checksum dua berkas yang disebut namanya, sehingga berkas lain di context tidak memengaruhi cache-nya.[4/5] COPY . .dieksekusi ulang — checksum kini dihitung dari seluruh isi context, dancatatan.txtmengubahnya; satu berkas kecil cukup untuk membatalkan cache salinan.EXPOSEikut 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.transferring context: 1076B— context yang dikirim kini memuat empat berkas; pada proyek nyata, angka ini bisa melonjak ke ratusan megabyte hanya karenanode_modulesikut serta.
Apakah catatan.txt benar-benar masuk ke dalam image eksperimen? Riwayat layer-nya menjawab:
$ docker history myapp:eksperimenOutput yang Dihasilkan:
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:
$ docker images myappOutput yang Dihasilkan:
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 130MBADD: 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:
| Aspek | COPY | ADD |
|---|---|---|
| Menyalin berkas dan folder biasa | Ya | Ya |
| Mengekstrak arsip tar lokal otomatis | Tidak | Ya |
| Mengunduh berkas dari URL | Tidak | Ya |
| Niat penulis Dockerfile terbaca jelas | Ya — perilakunya eksplisit | Tidak — perilaku implisit dan mengejutkan |
| Kapan sebaiknya dipakai | Hampir semua kasus | Hanya 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:
COPY --chown=appuser:appuser package.json server.js ./
COPY --chmod=755 entrypoint.sh /usr/local/bin/Mari kita telaah kedua flag di atas:
--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.--chmod=755— menetapkan izin berkas pada saat disalin, menggantikanRUN chmodyang menambah satu layer penuh hanya untuk mengubah satu angka.--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:
| Pola | Contoh | Alasan |
|---|---|---|
| Manifest dependensi disalin lebih dulu | COPY package.json go.mod Cargo.toml ./ | Cache instalasi dependensi bertahan saat kode berubah |
| Kode sumber disalin paling belakang | COPY src/ ./src/ setelah instalasi | Suntingan kode hanya membangun ulang blok terakhir |
| Berkas konfigurasi dipisahkan sendiri | COPY nginx.conf /etc/nginx/nginx.conf | Perubahan konfigurasi tidak memicu build ulang dependensi |
| Salinan selektif, bukan seluruh folder | COPY 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 #
- Dockerfile Reference
— definisi resmi
COPYdanADDbeserta seluruh flag-nya. - Dockerfile best practices
— panduan resmi kapan memakai
COPYatauADD. - Build context — cara Docker menafsirkan path sumber terhadap context.