Pembahasan EXPOSE menutup satu tikungan miskonsepsi: image myapp kini jujur mendokumentasikan port yang didengarkannya, dan kita tahu persis bahwa akses sepenuhnya diatur saat runtime. Dua instruksi berikutnya mengurus kenyamanan yang lebih mendasar: di dalam filesystem image, di direktori mana aplikasi hidup, dan sebagai user siapa aplikasi berjalan. Keduanya sering dianggap pelengkap kecil yang boleh dilewatkan — padahal tanpa WORKDIR, berkas aplikasi tercecer di direktori root bersama direktori sistem; tanpa USER, container berjalan sebagai root dengan seluruh hak akses administratif. Budi akan membuktikan kedua masalah itu lewat eksperimen kecil sebelum mematenkan pola yang benar pada image myapp.
WORKDIR Menetapkan Direktori Kerja #
Instruksi WORKDIR /app menetapkan direktori kerja untuk semua instruksi setelahnya. Path relatif pada COPY dan ADD diselesaikan relatif ke direktori itu, perintah RUN dijalankan dari dalam direktori itu, dan ketika container akhirnya berjalan, CMD atau ENTRYPOINT pun dieksekusi dengan direktori itu sebagai direktori kerja proses utama. Dua perilaku tambahan membuat WORKDIR nyaman dipakai: direktori yang ditunjuk dibuat otomatis bila belum ada — lengkap dengan seluruh folder induknya, seperti mkdir -p — dan nilai WORKDIR boleh berupa path relatif yang dirangkai ke WORKDIR sebelumnya. Efek gabungannya: satu baris instruksi memberi seluruh sisa Dockerfile sebuah “alamat rumah” yang konsisten.
Tanpa WORKDIR Berkas Mendarat di Root #
Untuk merasakan bedanya, Budi sengaja membangun image tanpa WORKDIR. Ia menyunting Dockerfile myapp menjadi versi telanjang berikut, membangunnya sebagai image eksperimen, lalu mengembalikan isinya seperti semula:
FROM node:22-alpine
COPY package.json server.js ./
CMD ["node", "server.js"]$ docker build -t myapp:akar .Output yang Dihasilkan:
[+] Building 2.9s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 77B 0.0s
=> [internal] load metadata for docker.io/library/node:22-alpine 0.7s
=> [1/2] FROM docker.io/library/node:22-alpine 0.0s
=> => resolve docker.io/library/node:22-alpine 0.7s
=> [2/2] COPY package.json server.js ./ 0.1s
=> => transferring context: 734B 0.0s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:9c2f5a8b1d4e6c3a7f0b2d8e5a1c4f6b3d9e2a5c8f1b4d6a0e3c7f5b8d2a1e4c 0.1s
=> => naming to docker.io/library/myapp:akar 0.0sDi mana kedua berkas aplikasi mendarat? Kita mengintipnya dengan menimpa perintah bawaan container lewat argumen docker run — pola command override yang kita kenal dari Chapter Command Override:
$ docker run --rm myapp:akar ls /Output yang Dihasilkan:
bin
dev
docker-entrypoint.sh
etc
home
lib
media
mnt
opt
package.json
proc
root
run
sbin
server.js
srv
sys
tmp
usr
varMari kita telaah isi direktori root di atas:
package.jsondanserver.jsberbaris dengan direktori sistem — tanpa WORKDIR, direktori kerja bawaan adalah/, sehingga./pada COPY menunjuk langsung ke root filesystem image.docker-entrypoint.shmilik image node — image dasar punya isi sendiri di direktori yang sama; berkas aplikasi yang tercecer di root membuka peluang tabrakan nama dengan isi basis.- Aplikasi masih bisa jalan —
node server.jsmenemukan berkasnya karena direktori kerja/kebetulan sama dengan lokasi berkas — tetapi struktur berantakan, dan pemilik image berikutnya harus menebak-menebak di mana kode berada.
Dengan WORKDIR Semua Punya Alamat #
Bayangan sebaliknya muncul pada image myapp yang selalu punya WORKDIR /app. Direktori kerjanya tersimpan sebagai konfigurasi image dan diwarisi proses apa pun yang dijalankan container:
$ docker run --rm myapp:1.1 pwdOutput yang Dihasilkan:
/appMari kita telaah satu kata /app di atas:
pwddijalankan di/app— instruksi WORKDIR pada Dockerfile tersimpan pada konfigurasi image, lalu menjadi direktori kerja bawaan untukCMD,ENTRYPOINT, dan proses tambahan apa pun.COPY package.json server.js ./pada image yang sama menempatkan berkas di/app/package.jsondan/app/server.js— alamat rapi yang mudah ditebak siapa pun yang membuka shell di dalam container.--rmmembersihkan jejak — container sekali pakai ini dibuang begitu perintahpwdselesai; pola yang pas untuk inspeksi cepat.
WORKDIR Bisa Dirangkai dan Memakai ENV #
WORKDIR boleh muncul berkali-kali dalam satu Dockerfile, dan nilai relatifnya dirangkai ke WORKDIR sebelumnya:
WORKDIR /app
WORKDIR src
WORKDIR tesMari kita telaah rangkaian di atas:
WORKDIR srcmenghasilkan/app/src— path relatif ditempel di belakang direktori kerja saat ini, persis perilakucd srcdi shell.WORKDIR tesmenghasilkan/app/src/tes— tiga baris instruksi membangun struktur tiga tingkat tanpa satu punRUN mkdir.- Setiap tingkat dibuat otomatis bila belum ada — memisah pembuatan direktori ke instruksi
RUNhanya menambah layer tanpa manfaat.
WORKDIR juga membaca variabel ENV — kombinasi yang membuat alamat aplikasi cukup dideklarasikan sekali:
ENV APP_DIR=/app
WORKDIR $APP_DIR
COPY package.json server.js ./Mari kita telaah kombinasi di atas:
$APP_DIRdievaluasi saat build — variabel ENV diganti nilainya sebelum WORKDIR diproses, sehingga instruksi menerima/app.- Satu sumber kebenaran — bila banyak instruksi memakai path yang sama, mengubah satu baris ENV menggeser seluruh alamat aplikasi sekaligus; materi variabel di Dockerfile sendiri sudah kita bedah tuntas pada Chapter ENV dan ARG.
USER Menentukan Identitas Proses #
USER bekerja dengan pola yang sama persis dengan WORKDIR, tetapi mengganti identitas, bukan lokasi. Setelah baris USER app, seluruh instruksi berikutnya — RUN, CMD, dan ENTRYPOINT — dijalankan sebagai user tersebut, baik saat build maupun saat container berjalan. Tanpa USER, semuanya berjalan sebagai root: user nomor 0 dengan seluruh hak administratif. Kondisi itu lumrah di lingkungan build yang terkontrol, tetapi container produksi yang berjalan sebagai root memperlebar permukaan risiko begitu aplikasi berhasil ditembus — pembahasan mendalamnya menanti di Section Keamanan Docker, khususnya Chapter Menjalankan Container Non-Root.
Buat User Terlebih Dulu Selagi Build Masih Root #
User tidak lahir sendiri: user harus dibuat lebih dulu dengan RUN, dan RUN itu sendiri harus berjalan sebagai root — artinya diletakkan sebelum baris USER. Pola lengkapnya pada image berbasis Alpine terlihat seperti ini:
FROM node:22-alpine
RUN apk add --no-cache curl
RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app
USER appmyapp sendiri tidak membutuhkan apk add — baris itu hanya memperagakan posisi pemasangan paket yang benar. Mari kita telaah urutan keempat baris di atas:
RUN apk add --no-cache curlberjalan sebagai root — pemasangan paket butuh hak administratif, dan instruksi memang belum disentuh USER; seluruh pekerjaan ber-akses-root harus diletakkan sebelum baris USER, bukan sesudahnya.RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app— pembuatan grup dan user juga menuntut hak root; keduanya digabung satu instruksi agar hanya menghasilkan satu layer.- Flag
addusermilik Alpine —-u 1000menetapkan UID,-G appmenjadikan grupappsebagai grup utama,-Hmencegah pembuatan folder home, dan-Dmembuat akun tanpa password; empat flag ini membentuk user utilitas yang minim jejak. USER appmemotong kewenangan mulai baris itu — instruksi sesudahnya, termasukCMDsaat container berjalan, sudah bukan root lagi.
myapp 1.4: Rumah Rapi dan Identitas Non-Root #
Budi mematenkan pola ini sebagai versi 1.4 dari image myapp — Dockerfile yang kini memuat rumah dan identitas sekaligus:
# syntax=docker/dockerfile:1
# Basis: Node.js 22 di atas Alpine
FROM node:22-alpine
# Metadata image
LABEL versi="1.4" deskripsi="API kecil milik Budi"
# Direktori kerja untuk semua instruksi berikutnya
WORKDIR /app
# Buat user dan grup non-root selagi build masih berjalan sebagai root
RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app
# Berkas aplikasi mendarat di /app
COPY package.json server.js ./
# Mulai baris ini semua instruksi berjalan sebagai user app
USER app
# Port yang didengarkan aplikasi
EXPOSE 8000
# Perintah bawaan container
CMD ["node", "server.js"]$ docker build -t myapp:1.4 .Output yang Dihasilkan:
[+] Building 4.3s (14/14) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 590B 0.0s
=> [internal] load metadata for docker.io/library/node:22-alpine 0.8s
=> [1/7] FROM docker.io/library/node:22-alpine 0.0s
=> => resolve docker.io/library/node:22-alpine 0.8s
=> [2/7] LABEL versi=1.4 deskripsi=API kecil milik Budi 0.0s
=> [3/7] WORKDIR /app 0.0s
=> [4/7] RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app 0.5s
=> [5/7] COPY package.json server.js ./ 0.1s
=> => transferring context: 1247B 0.0s
=> [6/7] USER app 0.0s
=> [7/7] EXPOSE 8000 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:7b3e9f2c81d46a0cbe4f2d9158e7a3c6f0b4d8e2a5c7f1b9d3e6a0c4f8b2d5e7 0.1s
=> => naming to docker.io/library/myapp:1.4 0.0sMari kita telaah output build di atas:
[4/7] RUN addgroup ... 0.5s— satu-satunya langkah yang benar-benar bekerja: layer tipis berisi perubahan pada/etc/group,/etc/passwd, dan/etc/shadowtempat identitas baru dicatat.[6/7] USER app0.0s — perpindahan identitas adalah metadata murni; tidak ada berkas yang ditulis, hanya konfigurasi image yang berubah.CMDtetap tidak tampil sebagai langkah bernomor — konfigurasi runtime dicatat saat ekspor image, pola yang sama dengan build pertama section ini.
Saatnya membuktikan identitasnya di lapangan. Budi menjalankan container tanpa flag port — kita tidak butuh akses host untuk memeriksa siapa yang bekerja di dalamnya:
$ docker run -d --name myapp-aman myapp:1.4Output yang Dihasilkan:
3f7a1c9e5b2d8f4a6c0e2b9d7f5a3c1e8b4d6f2a0c9e5b3d7f1a4c6e8b0d2f5adocker exec membuka pintu dua arah yang sudah kita kenal dari Chapter Menjalankan Perintah dalam Container:
$ docker exec myapp-aman whoamiOutput yang Dihasilkan:
app$ docker exec myapp-aman idOutput yang Dihasilkan:
uid=1000(app) gid=1000(app) groups=1000(app)Sebagai pembanding, image lama yang belum punya USER menjawab berbeda:
$ docker run --rm myapp:1.1 whoamiOutput yang Dihasilkan:
rootMari kita telaah tiga jawaban di atas:
appdari whoami — proses tambahan hasildocker execberjalan sebagai user container, yaitu user yang ditetapkan instruksi USER; proses utamanode server.jsberjalan dengan identitas yang sama.uid=1000(app) gid=1000(app)— angka inilah identitas sebenarnya bagi kernel; nama hanyalah label manusiawi yang ditempel pada angka.rootdari myapp:1.1 — seluruh container ber-image lama bekerja dengan wewenang penuh; membandingkan dua baris ini biasanya cukup untuk meyakinkan tim bahwa USER layak ditulis.
Menulis UID secara Eksplisit #
USER menerima nama (USER app) atau angka (USER 1000:1000), dan keduanya sah. Nama enak dibaca manusia, sedangkan angka lebih deterministik: nilai numerik tidak bergantung pada berkas /etc/passwd di dalam image sehingga tidak pernah gagal diterjemahkan, dan orchestrator yang memeriksa image pun membaca angka yang sama. Bagi kernel yang memeriksa izin berkas, yang dibandingkan selalu angka — itulah sebabnya konsistensi UID antar image menjadi urusan serius begitu data mulai berpindah. Berkas yang ditulis container ber-UID-1000 hanya bisa ditulis container lain ber-UID-1000; konflik angka antara image dan volume adalah salah satu sumber masalah penyimpanan paling klasik, dan kita bedah tuntas nanti pada Chapter Permission dan Ownership Volume.
WORKDIR versus USER #
Kedua instruksi ini kembar secara pola: sama-sama menetapkan konteks bagi instruksi setelahnya, sama-sama bisa ditimpa saat runtime. Tabel berikut merangkum keduanya berdampingan:
| Aspek | WORKDIR | USER |
|---|---|---|
| Menetapkan | Direktori kerja instruksi berikutnya | Identitas user instruksi berikutnya |
| Berlaku untuk | COPY, ADD, RUN, CMD, ENTRYPOINT | RUN, CMD, ENTRYPOINT |
| Nilai bawaan tanpa instruksi | / | root |
| Dibuat otomatis | Direktori dibuat bila belum ada | User harus dibuat lebih dulu lewat RUN |
| Efek pada container jalan | Direktori kerja proses utama | User yang menjalankan proses utama |
| Penimpa saat runtime | docker run –workdir | docker run –user |
Penimpaan runtime pada baris terakhir tabel bukan teori: docker run --workdir dan docker run --user mengubah keduanya per container tanpa menyentuh image, dan mekanisme lengkapnya adalah materi Chapter Working Directory serta Chapter User dan Permissions di Section Konfigurasi Container. Image myapp:1.4 kini punya alamat dan identitas: berkas hidup di /app, proses berjalan sebagai user app nomor 1000. Dua instruksi berikutnya menambahkan dua suara lain pada image — VOLUME menandai titik data yang layak dipisahkan dari umur container, dan LABEL menempelkan metadata identitas yang dibaca tool. Keduanya tampak sama jinaknya dengan EXPOSE, tetapi VOLUME menyimpan efek runtime yang diam-diam mengejutkan; mari kita lanjutkan ke Chapter VOLUME dan LABEL.
Referensi #
- Dockerfile Reference — definisi resmi WORKDIR dan USER beserta aturan penggabungan path dan bentuk UID.
- docker container exec — CLI Reference — referensi resmi exec yang kita pakai untuk memeriksa identitas di dalam container.
- Dockerfile best practices — panduan resmi pola membuat user non-root pada image aplikasi.