Environment variable menjawab bagaimana aplikasi dikonfigurasi, tetapi masih menyisakan pertanyaan identitas: sebagai user siapa proses container bekerja? Jawaban bawaan Docker mengejutkan mereka yang datang dari dunia server tradisional — hampir semua container berjalan sebagai root, user nomor 0 dengan seluruh hak administratif, kecuali image-nya sendiri menurunkan identitas lewat instruksi USER. Kebiasaan ini adalah warisan pola Dockerfile: sebagian besar pekerjaan build memang menuntut root, dan tanpa satu baris USER, identitas itu terbawa sampai container produksi menyala. Budi menemukan kenyataan tersebut saat mengaudit container myapp versi lama milik timnya, dan chapter ini membedah mengapa default-nya root, bagaimana flag --user menimpanya saat runtime, serta konsekuensi izin berkas dan port yang mengikuti angka UID tersebut.

Default root #

Mari kita periksa identitas container myapp versi pertama — versi yang dibangun sebelum instruksi USER masuk ke Dockerfile:

bash
$ docker run --rm myapp:1.0 whoami

Output yang Dihasilkan:

text
root
bash
$ docker run --rm myapp:1.0 id

Output yang Dihasilkan:

text
uid=0(root) gid=0(root) groups=0(root)

Sebagai pembanding, myapp versi 1.4 — image yang sudah memuat USER app sejak Chapter WORKDIR dan USER — menjawab berbeda:

bash
$ docker run --rm myapp:1.4 whoami

Output yang Dihasilkan:

text
app

Mari kita telaah tiga jawaban identitas di atas:

  1. root pada myapp:1.0 — Dockerfile versi itu tidak memuat instruksi USER, sehingga seluruh proses container berjalan sebagai user nomor 0; ini default Docker, bukan keputusan yang diambil Budi secara sadar.
  2. app pada myapp:1.4 — instruksi USER app pada Dockerfile menurunkan identitas sejak build; image yang sama kini menjalankan aplikasi sebagai user biasa ber-UID 1000.
  3. docker run --rm image whoami adalah pola command override — Chapter Command Override memberi kita alat untuk memeriksa identitas tanpa memasang shell interaktif; perintah id memperlihatkan angka-angka di balik nama.

Mengapa root menjadi masalah? Bila aplikasi di dalam container berhasil ditembus penyerang, penyerang tersebut mendarat sebagai root di dalam namespace container — bisa menulis ke seluruh filesystem container, menunggu celah kernel, dan bila konfigurasi host lenggang, mencoba melompat keluar. Namespace dan capabilities memang memagari container, tetapi pagar yang dipeluk dari dalam tetap lebih berbahaya daripada tidak ada orang di sana sama sekali; pembahasan menyeluruhnya menanti Section Keamanan Docker, dibuka oleh Chapter Prinsip Keamanan Container. Prinsip praktisnya satu kalimat: proses seharusnya berjalan dengan hak akses paling sedikit yang masih memungkinkan pekerjaannya selesai.

Flag –user untuk Menimpa Identitas #

Flag --user (bentuk pendek -u) menimpa identitas proses utama container saat runtime — tanpa menyentuh image sama sekali. Nilainya boleh berbentuk UID, pasangan uid:gid, nama user, atau pasangan nama dan grup. Kita coba pasangan angka pada image lama:

bash
$ docker run --rm --user 1000:1000 myapp:1.0 id

Output yang Dihasilkan:

text
uid=1000(node) gid=1000(node) groups=1000(node)
bash
$ docker run --rm --user 1000:1000 myapp:1.0 whoami

Output yang Dihasilkan:

text
node

Sekarang kita pakai UID yang tidak dimiliki siapa pun di dalam image:

bash
$ docker run --rm --user 2000:2000 myapp:1.0 whoami

Output yang Dihasilkan:

text
whoami: unknown uid 2000
bash
$ docker run --rm --user 2000:2000 myapp:1.0 id

Output yang Dihasilkan:

text
uid=2000 gid=2000

Mari kita telaah empat jawaban yang tampak saling bertentangan itu:

  1. Kernel hanya mengenal angka — identitas proses bagi kernel adalah UID dan gid numerik; nama hanyalah label manusiawi yang diterjemahkan lewat berkas /etc/passwd di dalam container.
  2. whoami berhasil mencari nama untuk UID 1000 — image node:22-alpine kebetulan membawa user bernama node dengan UID 1000, sehingga terjemahan nama ditemukan; kebetulan ini sekaligus menjelaskan mengapa angka 1000 populer sebagai UID aplikasi.
  3. whoami menyerah untuk UID 2000 — tidak ada entri /etc/passwd untuk 2000, sehingga tool menolak menerjemahkan; whoami keluar dengan exit code 1, keluarga galat yang sudah kita latih membacanya pada Chapter Memahami Exit Code.
  4. id tetap jujur menampilkan angka telanjang — proses memang berjalan sebagai UID 2000 tanpa masalah; kegagalan hanya ada pada penerjemahan nama. Flag --user karenanya tidak mensyaratkan user tersebut terdaftar di dalam image — inilah alasan menulis angka lebih deterministik daripada nama, kebiasaan yang kita sepakati sejak Chapter WORKDIR dan USER.

Konsekuensi terhadap Izin Berkas #

Identitas baru membawa aturan izin berkas ke mana-mana: berkas yang dimiliki root tidak lagi bisa ditulis, dan volume yang dibuat Docker dimiliki root secara bawaan. Kita buktikan dengan container alpine yang mencoba menulis ke volume segar:

bash
$ docker run --rm -v /data --user 1000:1000 alpine:3.20 touch /data/berkas.txt

Output yang Dihasilkan:

text
touch: /data/berkas.txt: Permission denied

Mari kita telaah kegagalan yang sangat lazim itu:

  1. Volume anonim diciptakan dan dimiliki root:root — daemon membuat volume baru dengan kepemilikan penuh milik root; proses UID 1000 tidak punya hak menulis ke dalamnya.
  2. Permission denied dengan exit code 1 — kernel yang menolak operasi tulis, bukan Docker; angka UID yang tidak cocok adalah satu-satunya alasannya.
  3. Sumber masalah yang sama berlaku untuk bind mount — direktori milik UID host tertentu hanya bisa ditulis oleh UID yang sama dari sisi container; konflik angka antara user host, image, dan volume adalah salah satu kisah paling klasik di dunia penyimpanan Docker, dan kita bedah tuntas nanti pada Chapter Permission dan Ownership Volume.

Port Rendah dan NET_BIND_SERVICE #

Identitas juga berurusan dengan port. Kernel Linux melarang proses non-root mengikat port di bawah 1024 kecuali proses itu memegang capability NET_BIND_SERVICE. Container Docker bawaan justru menyertakan capability tersebut dalam daftar 12 capabilities default-nya — daftar yang kita bedah nanti pada Chapter Linux Capabilities — sehingga container non-root di Docker masih bisa mendengarkan port 80. Namun mengandalkan keberuntungan capabilities bukan kebiasaan yang sehat, dan pola yang jauh lebih elegan sudah kita pegang sejak Section Dasar Docker CLI:

Dockerfile USER versus –user Runtime #

Dua cara menetapkan user — instruksi di Dockerfile dan flag saat docker run — bekerja pada waktu yang berbeda dan tidak saling menghapus. Tabel berikut merangkum hubungan keduanya:

AspekUSER di Dockerfile–user saat docker run
Menetapkan saatbuild — menempel permanen di imagecreate — hanya untuk container itu
Nilai bawaanroot bila instruksi tidak ditulistanpa –user, mengikuti USER image
Bentuk nilainama user atau angka UIDuid, uid:gid, nama, atau nama:grup
Kapan menanghanya bila runtime tidak menyebut –user–user selalu menimpa USER image
Menyentuh image?ya — image hasil build berubahtidak sama sekali
Dipakai olehsetiap container dari image ituproses utama container yang bersangkutan

Catatan tambahan untuk docker exec: perintah itu punya flag -u sendiri, sehingga proses tambahan bisa berjalan sebagai identitas lain — misalnya masuk sebagai root ke container yang aplikasinya non-root untuk perbaikan darurat, tanpa mengubah identitas proses utamanya.

Image Resmi yang Menurunkan Identitas Sendiri #

Kebanyakan image resmi besar tidak menunggu kita menurunkan identitas — mereka melakukannya sendiri dengan pola dua tingkat: proses persiapan berjalan sebagai root, proses pemakaian jalan sebagai user rendah. Container web nginx yang lama kita kenal memperagakannya secara hidup. Budi menjalankan ulang container kebiasaan Section Dasar Docker CLI:

bash
$ docker rm -f web
$ docker run -d --name web -p 8080:80 nginx

Output yang Dihasilkan:

text
web
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a

Perintah docker top membedah proses container dari sisi host — tanpa membutuhkan tool ps di dalam image, berbeda dari docker exec yang menuntut biner tersedia di filesystem container:

bash
$ docker top web

Output yang Dihasilkan:

text
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                4217                4185                0                   09:14               ?                   00:00:00             nginx: master process nginx -g daemon off;
101                 4218                4217                0                   09:14               ?                   00:00:00             nginx: worker process
101                 4219                4217                0                   09:14               ?                   00:00:00             nginx: worker process

Mari kita telaah tiga baris proses di atas:

  1. Master berjalan sebagai root — proses induk nginx membaca konfigurasi, membuka port 80 di dalam container, lalu menurunkan identitas anak-anaknya; entrypoint image menjalankan nginx -g daemon off; persis seperti yang terbaca pada kolom CMD.
  2. Worker berjalan sebagai UID 101 — angka itu milik user nginx yang dibuat oleh image nginx; penurunan dilakukan lewat directive user nginx; pada nginx.conf, bukan lewat instruksi USER Dockerfile.
  3. Kolom UID diisi angka, bukan nama — docker top menjalankan ps milik host dan ps menampilkan UID numerik tanpa menerjemahkannya ke nama user host; angka 101 milik user nginx di dalam container, sedangkan di sisi host angka yang sama bisa saja tidak dimiliki siapa pun — persis situasi unknown uid pada demo --user 2000:2000 tadi.

Pola serupa berlaku luas di keluarga image resmi: postgres menurunkan dirinya ke user postgres dengan UID 999 lewat entrypoint-nya, redis beralih ke user redis setelah entrypoint memperbaiki kepemilikan direktori datanya. Karena itu sebelum menambahkan --user pada image resmi, periksa dulu dokumentasinya — menimpa identitas yang sudah diturunkan dengan angka yang salah justru menciptakan masalah izin berkas baru, persis skenario Permission denied yang kita buktikan di atas.

Identitas proses kini berada dalam kendali kita, dan tip di tengah chapter sempat menyebut satu flag yang bekerja sama erat dengannya: -p 80:8080. Flag tersebut terlihat seperti trik kecil untuk memindahkan nomor port, padahal formatnya menyimpan empat segmen — alamat host, port host, port container, dan protokol — yang masing-masing punya aturan main sendiri. Chapter Port Publishing membedah format lengkap itu, dari pengikatan ke 127.0.0.1 sampai rentang port dan konflik yang paling sering menyapa pemula.

Referensi #