Ringkasan Utama
Cara menyimpan secret environment variable Docker dengan aman: jangan taruh di ENV atau ARG, karena keduanya berakhir di image. Simpan di file .env yang diabaikan Git dan .dockerignore, kirim ke container saat runtime lewat --env-file atau env_file, pakai secret mount untuk kredensial build, dan segera putar ulang kunci yang pernah ter-commit.
Aplikasimu jalan di laptop karena ada file .env berisi API key. Lalu kamu mau menjalankannya di container, dan muncul pertanyaan yang hampir semua pemula alami: kuncinya ditaruh di mana? Jalan pintasnya terlihat jelas, yaitu menulis ENV API_KEY=... di Dockerfile. Itu jalan yang membuat kunci ikut terkirim ke registry bersama image, terbaca oleh siapa pun yang bisa menarik image itu.
Panduan ini membandingkan empat tempat untuk sebuah nilai: ARG, ENV, environment variable saat runtime, dan secret mount, lalu membawanya sampai GitHub Actions dan apa yang harus dilakukan kalau kunci terlanjur masuk Git. Semua perintah dicocokkan dengan dokumentasi resmi Docker dan GitHub, dan pola deploy-nya sama dengan yang dipakai situs ini: secret disimpan di GitHub, ditulis ke file .env milik pemilik saja di server, lalu dibaca container saat start. Semua nilai di lab adalah karangan.
Mau memahami konsepnya dulu, lengkap dengan animasi alurnya? Baca pelajaran gratis kami: Belajar environment variable dan secret Docker. Artikel ini sisi prakteknya: kamu mengetik perintah dan melihat sendiri di mana nilai bocor dan di mana aman.
Yang kamu butuhkan
Tidak banyak. Semua langkah berjalan di Linux, macOS, maupun Windows selama Docker aktif.
- Docker terpasang dan berjalan (Docker Desktop di macOS atau Windows, Docker Engine di Linux).
- Terminal dan satu editor teks. Docker Compose dipakai di Langkah 4; versi Docker modern sudah menyertakannya.
- Sekitar 30 menit dan folder kosong bernama secret-lab.
docker --version
# Expect one line starting with: Docker version ...
# "command not found" or "cannot connect to the Docker daemon" means
# Docker is not installed or not running yet. Fix that first.
mkdir secret-lab && cd secret-lab
# Every value below is made up. Never paste a real key into a lab.Pakai nilai karangan seperti not-a-real-secret. Jangan pernah menempelkan kunci asli ke lab latihan, apalagi ke perintah yang masuk riwayat terminal.
Langkah 1: Lihat sendiri kenapa ARG dan ENV bukan tempat secret
Mulai dari ARG. Dokumentasi Dockerfile menyatakan nilai ARG tidak tertanam di image dan tidak ada di container akhir, tetapi build argument tetap terlihat di docker history, dan Docker tidak menyarankan memakainya untuk secret. Buktikan sendiri:
# Dockerfile.arg
FROM alpine:3
ARG DEMO_TOKEN
RUN test -n "$DEMO_TOKEN"
# in a terminal:
docker build --build-arg DEMO_TOKEN=not-a-real-secret -f Dockerfile.arg -t env-lab:arg .
docker history --no-trunc env-lab:arg | grep not-a-real-secret
# If this prints a line, the value is readable by anyone who can pull the image.Sekarang ENV. Dokumentasi menyebut nilai ENV selalu disimpan di image hasil build dan terbawa ke setiap container yang dijalankan darinya. Tidak perlu docker history; cukup jalankan containernya:
# Dockerfile.env
FROM alpine:3
ENV DEMO_TOKEN=not-a-real-secret
# in a terminal:
docker build -f Dockerfile.env -t env-lab:env .
docker run --rm env-lab:env printenv DEMO_TOKEN
# not-a-real-secret <- it ships inside the image and every container started from itKalau sebuah kunci pernah masuk ARG atau ENV lalu image-nya di-push, anggap kunci itu bocor. Putar ulang (rotate) di penyedianya; menghapus image saja tidak menarik kembali salinan yang sudah ditarik orang lain.
Ringkasnya, ENV dan ARG cocok untuk nilai yang boleh dibaca siapa pun, misalnya NODE_ENV atau nomor port. Dockerfile situs ini hanya berisi ENV seperti itu dan tidak mendeklarasikan satu pun ARG.
Langkah 2: Jauhkan file .env dari Git dan dari image
Dua file mencegah dua kebocoran berbeda. .gitignore menjaga .env dari repository. .dockerignore menjaga .env dari build context, dan Docker menyatakan file yang cocok dengan polanya dihapus dari build context sebelum dikirim ke builder, sehingga tidak bisa disalin ke image dengan ADD, COPY, atau bind mount. Repo situs ini mencantumkan .env dan .env.* di kedua file.
# .gitignore (keeps the file out of Git)
.env
.env.*
# .dockerignore (keeps the file out of the build context)
.env
.env.*# Dockerfile
# Wrong: without a .dockerignore, this copies a local .env into a layer.
COPY . .
# Right: same line, but .dockerignore removed .env from the context first,
# so there is nothing for COPY to pick up.
COPY . .Perhatikan bahwa .gitignore hanya berlaku untuk file yang belum pernah di-commit. Kalau .env sudah terlanjur masuk Git, menambahkannya ke .gitignore sekarang tidak menghapus riwayatnya; itu urusan Langkah 7.
Langkah 3: Kirim secret saat runtime dengan --env-file atau -e
Pola yang benar: image dibangun tanpa secret, lalu nilainya diberikan ke container ketika ia dijalankan. Dengan --env-file, setiap baris berbentuk VAR=value, dan baris yang diawali tanda # adalah komentar. Dengan -e VAR tanpa tanda sama dengan, Docker CLI mengambil nilai VAR dari environment shell-mu.
# A file of KEY=value lines. Lines starting with # are comments.
printf 'DEMO_TOKEN=not-a-real-secret\n' > .env
docker run --rm --env-file .env alpine:3 printenv DEMO_TOKEN
# not-a-real-secret
# Or pass one variable straight from your shell: no "=" means "copy it from here".
export DEMO_TOKEN=not-a-real-secret
docker run --rm -e DEMO_TOKEN alpine:3 printenv DEMO_TOKEN
# not-a-real-secretSatu jebakan kecil: dokumentasi menyatakan tanda # di tengah baris adalah bagian dari nilai, bukan komentar. Jadi komentar di akhir baris ikut masuk ke dalam secret-mu dan membuat autentikasi gagal tanpa pesan jelas.
# Wrong: the # is part of the value, not a comment.
DEMO_TOKEN=abc123 # my staging key
# Right: comment on its own line.
# my staging key
DEMO_TOKEN=abc123Pakai -e VAR tanpa nilai ketika kamu tidak mau mengetik kunci di baris perintah. Kalau variabelnya tidak di-export di shell, dokumentasi menyebut variabel itu tidak diatur di container.
Langkah 4: Pindahkan ke Docker Compose dengan env_file
Di server, kamu hampir pasti memakai Compose. Atribut env_file memuat file .env ke environment container, dengan path relatif terhadap lokasi compose.yaml. Setelah file dibuat, batasi izinnya dengan chmod 600 supaya hanya pemilik yang bisa membacanya, seperti yang dilakukan deploy situs ini.
# compose.yaml
services:
web:
image: alpine:3
env_file: .env
# .env
GREETING=from-env-file
# in a terminal:
chmod 600 .env # owner-only, as this site does on its server
docker compose run --rm web printenv GREETING
# from-env-fileKalau nama yang sama diatur di dua tempat, Compose punya urutan tetap. Dari yang tertinggi: docker compose run -e, lalu environment atau env_file dengan nilai hasil interpolasi, lalu environment saja, lalu env_file saja, dan terakhir ENV di image. Karena itu entri environment menang atas env_file, seperti pada contoh di atas.
services:
web:
image: alpine:3
env_file: .env
environment:
GREETING: from-compose-file # environment outranks env_file
# docker compose run --rm web printenv GREETING
# from-compose-file
# For a value that MUST exist, fail loudly instead of starting with a blank:
# image: ${IMAGE:?IMAGE is required}Satu catatan jujur: dokumentasi Compose menyarankan agar password tidak diteruskan lewat environment variable dan menawarkan Compose secrets. env_file tetap masuk akal untuk satu container di satu server dengan file milik pemilik saja, seperti situs ini. Untuk setup yang lebih besar, pelajari opsi secrets itu.
Langkah 5: Pakai secret mount kalau build-nya yang butuh kredensial
Kadang build perlu kredensial, misalnya untuk mengunduh package privat. Docker menulis bahwa build argument dan environment variable tidak pantas untuk ini karena bertahan di image akhir, dan menyarankan secret mount. Nilainya muncul sebagai file di /run/secrets/id-nya hanya selama satu instruksi RUN itu.
# Dockerfile.secret
FROM alpine:3
# The secret exists only during this one RUN, at /run/secrets/demo.
RUN --mount=type=secret,id=demo test -s /run/secrets/demo
# in a terminal:
printf 'not-a-real-secret' > demo.txt
docker build --secret id=demo,src=demo.txt -f Dockerfile.secret -t env-lab:secret .
docker history --no-trunc env-lab:secret | grep not-a-real-secret
# No output: the value is not in the history.
# From an environment variable instead of a file:
# docker build --secret id=DEMO_TOKEN .Situs ini tidak memerlukannya, jadi bagian ini murni dari dokumentasi Docker. Untuk secret yang dibutuhkan aplikasi saat berjalan, tetap pakai Langkah 3 dan 4.
Langkah 6: Bawa secret dari GitHub Actions ke server
Simpan nilainya di GitHub sebagai secret dan baca lewat konteks secrets. Dokumentasi GitHub menyarankan meneruskan secret ke perintah sebagai environment variable yang dirujuk dengan tanda kutip, dan memperingatkan agar menghindari secret di baris perintah karena proses lain bisa melihatnya lewat ps.
# Wrong: the secret sits in the command line, where other users of the
# machine can see it with ps.
- run: ./deploy.sh --token ${{ secrets.DEPLOY_TOKEN }}
# Right: hand it over as an environment variable and quote the reference.
- env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./deploy.sh "$DEPLOY_TOKEN"Deploy situs ini menulis file .env di server lewat SSH dengan heredoc bertanda kutip, lalu chmod 600. Bentuknya begini, dengan nama yang diganti placeholder:
# Inside the SSH step of the deploy job (names are placeholders).
# The quotes around ENVEOF stop the shell expanding a $ inside a value.
cat > .env <<'ENVEOF'
API_KEY=${{ secrets.API_KEY }}
APP_PORT=${{ vars.APP_PORT || 3002 }}
ENVEOF
chmod 600 .envJangan mengandalkan penyamaran log. GitHub memang menyamarkan secret yang tercetak, tetapi dokumentasinya menyatakan penyamaran itu tidak dijamin karena nilai bisa diubah bentuknya, dan runner hanya bisa menyamarkan secret yang dipakai di job saat ini. Jangan pernah mencetak secret dengan echo.
Langkah 7: Kalau secret terlanjur ter-commit atau ter-push
Urutannya penting. Dokumentasi GitHub menyebut langkah pertama adalah mencabut atau memutar ulang secret itu; setelah diputar, kunci lama tidak bisa dipakai lagi, dan menulis ulang riwayat mungkin tidak perlu. Kalau kamu tetap ingin membersihkan riwayat, GitHub menyarankan git-filter-repo versi 2.47 atau lebih baru dengan opsi --sensitive-data-removal:
# 1. Revoke or rotate the key at the provider. Do this before anything else.
# 2. Optional: rewrite history to drop the file (git-filter-repo 2.47 or later).
git-filter-repo --sensitive-data-removal --invert-paths --path .env
# 3. Publish the rewritten history.
git push --force --mirror originMenulis ulang riwayat dan force push tidak cukup. GitHub memperingatkan commit lama masih bisa diakses di clone dan fork lain, lewat tampilan cache berdasarkan hash SHA-1, dan lewat pull request yang merujuknya. Itulah alasan putar ulang kunci selalu langkah pertama.
Setelah itu, kembali ke Langkah 2 supaya kejadiannya tidak berulang. Pelajari juga cara kerja secret scanning di artikel pendukung di bagian akhir.
Error umum dan cara memperbaikinya
Enam gejala yang paling sering membuat pemula bingung, beserta penyebab dan perbaikannya.
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Kunci terlihat di output docker history | Nilai dikirim lewat ARG atau build-arg | Putar ulang kunci, lalu pindah ke RUN --mount=type=secret |
| printenv di container kosong padahal ada di .env | Lupa --env-file atau env_file, atau memakai -e VAR yang tidak di-export di shell | Tambahkan --env-file .env atau env_file: .env, atau export variabelnya dulu |
| Autentikasi gagal padahal kunci benar | Komentar di akhir baris .env ikut menjadi bagian nilai | Pindahkan komentar ke baris sendiri yang diawali tanda # |
| Nilai dari environment dipakai, bukan dari .env | Dalam prioritas Compose, environment berada di atas env_file | Hapus entri yang ganda, atau sengaja pakai environment sebagai override |
| Compose jalan dengan nilai kosong tanpa peringatan | Variabel belum diatur, dan interpolasi biasa memakai string kosong | Pakai bentuk wajib ada dengan tanda tanya dan pesan error, supaya Compose berhenti dengan error |
| Secret tetap muncul di log GitHub Actions | Penyamaran tidak dijamin untuk nilai yang diubah bentuknya, dan hanya berlaku untuk secret di job itu | Jangan mencetak secret; putar ulang kunci yang sempat tercetak |
Semua gejala di atas bisa kamu reproduksi di lab ini dengan nilai karangan, jadi coba sengaja salah dulu supaya mengenali wajahnya.
Checklist sebelum deploy
- .env dan .env.* ada di .gitignore dan .dockerignore.
- Dockerfile hanya berisi ENV yang tidak sensitif, dan tidak ada ARG untuk secret.
- Secret aplikasi diberikan saat runtime lewat env_file atau --env-file.
- File .env di server dibatasi dengan chmod 600.
- Secret di GitHub Actions dirujuk lewat env dengan tanda kutip, tidak ditulis di baris perintah dan tidak dicetak.
- Kamu tahu urutan kalau kunci bocor: putar ulang dulu, baru bersihkan riwayat.
Belajar lebih dalam
Untuk gambaran besar tempat secret berada di setiap tahap deploy, ikuti pelajaran Belajar environment variable dan secret Docker, lengkap dengan kuis dan animasi. Untuk kebiasaan Dockerfile yang baik secara umum, baca praktik terbaik containerization Docker. Dan kalau kamu ingin melangkah ke Docker secrets di production, lanjut ke Docker secrets di production.
Aturan yang bisa kamu bawa pulang: image dibangun tanpa secret, secret datang saat runtime, dan kunci yang pernah terlihat orang lain dianggap bocor dan diputar ulang. Sisanya hanyalah memilih tempat yang tepat untuk setiap nilai.
Sumber dan referensi:
- Dokumentasi Docker: referensi Dockerfile (ARG dan ENV)
- Dokumentasi Docker: build secrets
- Dokumentasi Docker: docker container run (-e dan --env-file)
- Dokumentasi Docker: prioritas environment variable di Compose
- Dokumentasi GitHub: menggunakan secrets di GitHub Actions
- Dokumentasi GitHub: menghapus data sensitif dari repository

