Env Var dan Secret Docker: Build Time vs Runtime
Pelajari env var dan secret di deploy Docker: build time vs runtime, GitHub secrets, env_file, dan kenapa ENV serta ARG tidak boleh menyimpan secret.
Oleh Matthews WongDitinjau pada
Poin utama
Pada deploy Docker, nilai build time tertanam di image, sedangkan nilai runtime diberikan ke container saat ia dimulai. Secret hanya boleh ada di runtime: disimpan di GitHub, ditulis ke file env yang hanya bisa dibaca pemiliknya di server, lalu dibaca container. ENV dan ARG tidak boleh menyimpan secret, karena keduanya berakhir di image.
Setelah pelajaran ini, Anda bisa
- Memilah setiap setelan ke build time atau runtime, dan menyebut di mana secret boleh dan tidak boleh berada.
- Menelusuri sebuah GitHub secret dari repository sampai ke process.env di dalam container.
- Memprediksi sumber Compose mana yang menang ketika variabel yang sama diatur dua kali.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Di mana secret berada di setiap tahap deploy
Image dibangun tanpa secret. Secret tetap di GitHub sampai job deploy menuliskannya ke file env di server, dan Compose memberikan file itu ke container saat container dimulai.
Tombol: ← → langkah · Home / End · K putar atau jeda
Di kiri, GitHub membangun image dan menyimpan secret. Di kanan, server menyimpan sebuah file dan menjalankan container. Ikuti ke mana secret pergi.
Build hanya melihat file repository. .dockerignore menjaga file .env di luar build context, dan Dockerfile tidak mendeklarasikan ARG, jadi image hanya membawa kode dan ENV yang tidak sensitif.
.env .env.*Tebak langkah berikutnya Image tidak berisi secret. Di mana secret aplikasi menunggu sampai server membutuhkannya? (Di Dockerfile sebagai ENV · Di secret terenkripsi milik GitHub · Di file .env milik repository)GitHub menyimpan secret terenkripsi, dan workflow hanya bisa membaca secret yang ia sebut secara eksplisit. Di sini hanya job deploy yang menyebut secret aplikasi.
secrets context vars contextLewat SSH, job deploy menulis nilai-nilai itu ke .env dengan heredoc bertanda kutip, lalu menjadikan file itu hanya untuk pemilik. Nilainya tidak pernah menyentuh image.
write .env chmod 600 .envTebak langkah berikutnya Bagaimana container yang berjalan mendapatkan nilai-nilai itu? (Compose meneruskan file itu lewat env_file · Aplikasi mengunduhnya dari GitHub · Nilai itu sudah tertanam di image)Compose menarik image bertag SHA dan meneruskan .env ke container lewat env_file. File yang sama juga mengisi substitusi IMAGE dan APP_PORT di file Compose.
env_file: .envKode server membaca process.env saat berjalan, jadi satu image bisa dipakai dengan .env mana pun. Pengecualiannya nilai NEXT_PUBLIC, yang dibekukan Next.js ke dalam JavaScript saat build.
Apa beda konfigurasi build time dan runtime?
Build time adalah saat docker build berjalan: setiap instruksi meninggalkan layer, dan apa pun yang diatur di sana bisa berakhir di dalam image. Runtime adalah saat sebuah container dimulai dari image itu dan diberi environment-nya. Image dibagikan, disalin ke registry, dan ditarik oleh server, jadi apa pun yang tertanam ikut bepergian bersamanya. Environment hanya diberikan ke satu container di satu mesin.
Baca selengkapnya (2)Tutup
Referensi Dockerfile dari Docker tegas soal dua instruksi yang terlihat seperti konfigurasi. Nilai ENV selalu disimpan di image hasil build dan terbawa ke setiap container yang dijalankan darinya. Nilai ARG tidak tertanam di image dan tidak tersedia di container akhir, tetapi build argument tetap bisa dilihat lewat docker history, dan Docker menyatakan jelas bahwa meneruskan secret lewat keduanya tidak direkomendasikan.
Ketika build benar-benar butuh kredensial, misalnya untuk mengambil package privat, jawaban Docker adalah secret mount: RUN --mount=type=secret membuat nilainya tersedia hanya untuk satu instruksi itu, secara default sebagai file di bawah /run/secrets, dan diberikan lewat baris perintah dengan --secret. Situs ini tidak pernah membutuhkannya. Dockerfile-nya tidak mendeklarasikan ARG, dan satu-satunya baris ENV hanya menyetel nilai tak sensitif seperti port.
Kenapa variabel NEXT_PUBLIC berbeda dari variabel server?
Next.js membagi variabel menurut tempat ia boleh dibaca. Tanpa prefix, variabel hanya ada di lingkungan Node.js. Dengan prefix NEXT_PUBLIC_, Next.js menyisipkan nilainya ke bundle JavaScript saat next build, mengganti setiap referensi dengan nilai yang tertulis langsung dan bisa dibaca siapa pun di browser. Jadi nilai NEXT_PUBLIC tidak pernah bisa menjadi secret.
Baca selengkapnya (2)Tutup
Penyisipan yang sama menjadikannya konfigurasi build time. Dokumentasi Next.js mencatat bahwa setelah build, aplikasi tidak lagi merespons perubahan variabel itu, dan bahwa satu image Docker yang dipromosikan ke beberapa environment membekukannya pada nilai saat build. Pencarian dinamis seperti process.env[name] tidak disisipkan sama sekali. Variabel server adalah kebalikannya: ia bisa dibaca saat request ketika rendering dinamis, dan itulah yang membuat satu image bisa berjalan dengan nilai berbeda.
Dua secret situs ini, kunci penyedia AI dan kunci penyedia email, hanya ada di sisi server. Keduanya dibaca dari process.env di dalam route handler API di server, tidak pernah di komponen yang dikirim ke browser. Itulah bentuk yang patut ditiru: identifier publik di build time, secret di runtime di server.
Bagaimana GitHub secrets dan variables sampai ke server?
GitHub secrets dienkripsi sebelum sampai ke GitHub, dengan libsodium sealed box, dan bisa berada di level organisasi, repository, atau environment. Workflow hanya bisa membaca secret yang ia sebut secara eksplisit lewat secrets context. Secret tidak diteruskan ke runner ketika workflow dipicu dari fork, tidak otomatis diteruskan ke reusable workflow, dan tidak tersedia untuk workflow yang dipicu Dependabot, jadi step yang mengharapkannya akan menerima kosong.
Baca selengkapnya (2)Tutup
Konfigurasi biasa masuk ke variables, dibaca lewat vars context, dan variabel yang belum diatur mengembalikan string kosong. Itu sebabnya workflow menulis vars.APP_PORT || 3002: nilai kosong jatuh ke default. Aturan praktis di situs ini: apa pun yang berbahaya bila diposting publik adalah secret, selebihnya variable.
GitHub mengevaluasi setiap ekspresi dollar-kurung-kurawal dan mengganti hasilnya sebelum script shell berjalan, sehingga ketika step SSH dieksekusi, heredoc sudah berisi nilai sebenarnya. Bash menyatakan bahwa jika bagian mana pun dari delimiter heredoc diberi tanda kutip, baris-barisnya tidak diekspansi, itulah sebabnya delimiter ditulis 'ENVEOF' dengan tanda kutip: nilai yang mengandung tanda dolar tetap literal dan tidak dianggap variabel.
Bagaimana Docker Compose mengubah .env menjadi environment container?
File .env di sini punya dua tugas. Diletakkan di samping file Compose, ia memberi data untuk interpolasi: ${IMAGE} dan ${APP_PORT:-3002} di YAML diganti dari file itu, dengan bentuk :- memakai default ketika variabel belum diatur atau kosong, dan ${VAR:?error} berhenti dengan error. Lalu env_file: .env memuat file yang sama ke environment container. Interpolasi adalah fitur Compose, yang tidak didukung docker run --env-file.
Baca selengkapnya (2)Tutup
Ketika satu variabel diatur di beberapa tempat, Compose menyelesaikannya dengan urutan tetap, dari yang tertinggi: docker compose run -e, lalu entri environment atau env_file dengan nilai hasil interpolasi, lalu environment saja, lalu env_file saja, dan terakhir ENV yang tertanam di image. Jadi ENV di Dockerfile hanya berlaku bila Compose tidak mengatur apa pun untuk nama itu. Dengan beberapa entri env_file, file yang belakangan menimpa yang lebih dulu.
Ada trade-off yang jujur di sini. Dokumentasi Compose sendiri menyarankan agar password dan API key tidak diteruskan sebagai environment variable, karena sering tersedia untuk semua proses di container dan sulit dilacak, dan menawarkan Compose secrets, yang di-mount sebagai file di bawah /run/secrets. Situs ini tetap memakai env_file: satu container di satu mesin, file yang hanya bisa dibaca pemiliknya, dan secret yang hanya ada di server. Ketahui bahwa opsi yang lebih kuat itu ada sebelum memilih yang lebih sederhana.
Bagaimana menjaga secret tetap di luar image, repository, dan log?
Di luar repository: .gitignore mencantumkan file .env, dan dokumentasi Next.js sendiri menyebut hampir tidak pernah Anda ingin meng-commit-nya. Di luar image: .dockerignore mengecualikan .env dan .env.* dari build context, dan aturan Docker menyatakan file yang dikecualikan tidak bisa disalin oleh instruksi COPY, jadi COPY . . yang ceroboh tidak bisa ikut menyapunya.
Baca selengkapnya (2)Tutup
Di luar log: GitHub menyamarkan secret yang tercetak ke log, tetapi dokumentasinya menyebut penyamaran itu tidak dijamin, karena secret bisa diubah bentuknya dengan banyak cara, dan runner hanya bisa menyamarkan secret yang dipakai di job saat ini. Jadi jangan pernah mencetaknya. Jika sebuah secret menghasilkan nilai sensitif lain, daftarkan nilai itu untuk disamarkan juga, dan hindari membungkus beberapa secret dalam satu blob JSON atau YAML; buat satu secret per nilai.
Dan siapkan diri untuk hari ketika satu secret bocor. Panduan hardening GitHub menyarankan pemberian izin minimum, audit cara secret dipakai, dan rotasi berkala untuk memperpendek masa berlaku secret yang bocor. Workflow ini menerapkan aturan pertama pada token-nya sendiri: setiap job hanya mencantumkan izin yang dibutuhkannya.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
# Write runtime env from GitHub Secrets/Variables. Quoted heredoc so
# shell does not expand anything in the (already-rendered) values.
cat > .env <<'ENVEOF'
GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}
RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
CONTACT_FROM_EMAIL=${{ vars.CONTACT_FROM_EMAIL }}
CONTACT_TO_EMAIL=${{ vars.CONTACT_TO_EMAIL }}
IMAGE=${{ needs.build-and-push.outputs.image }}
APP_PORT=${{ vars.APP_PORT || 3002 }}
ENVEOF
chmod 600 .envSatu-satunya tempat secret dan variable aplikasi bertemu. Dua baris pertama adalah secret, dua berikutnya variable, lalu IMAGE dan APP_PORT, yang dipakai Compose untuk substitusi. Tanda kutip di sekitar ENVEOF mencegah shell mengekspansi tanda dolar di dalam nilai, dan chmod 600 membuat file hanya bisa dibaca pemiliknya.
# Runtime compose used on the VPS. The image is built + pushed to GHCR by
# .github/workflows/deploy.yml; this file is copied to the server, and CI
# writes a sibling .env (from GitHub Secrets/Variables) that supplies both the
# ${IMAGE}/${APP_PORT} substitutions below and the container's runtime env.
services:
web:
image: ${IMAGE:-ghcr.io/matthews-wong/matthews-porto-nextjs:latest}
# Matches the existing container so deploys UPDATE it in place (no duplicate).
container_name: matthewswong-portfolio
restart: unless-stopped
env_file: .envKomentar di bagian atas menyatakan kontraknya: CI menulis .env di sebelahnya yang menyuplai substitusi sekaligus env runtime container. IMAGE punya default setelah :- sehingga file tetap valid tanpa CI. env_file: .env adalah baris yang menyerahkan setiap nilai ke container.
node_modules
.git
.env
.env.*Dua baris .env adalah penjaga untuk image. .env dan .env.* tidak pernah masuk ke build context, jadi tidak ada COPY di Dockerfile yang bisa menaruh file secret lokal ke dalam layer, bahkan di laptop yang memilikinya.
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0Setiap ENV di stage runner tidak sensitif: flag mode, saklar telemetri, port, dan alamat bind. Aman ditanam di image karena nilainya sama di mana-mana dan boleh dibaca siapa pun. Tidak ada satu pun di sini, atau di bagian lain file ini, yang berupa kredensial.
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}Tidak setiap kredensial adalah secret yang disimpan. Login registry memakai GITHUB_TOKEN milik job, yang dibuat GitHub per job, jadi tidak ada password registry berumur panjang yang perlu disimpan atau bisa bocor. Secrets context membacanya, tetapi Anda tidak pernah membuatnya.
Kesalahan yang benar-benar membuang waktu
Secret yang dikirim sebagai ARG atau disetel dengan ENV
ENV selalu tersimpan di image, dan build argument bisa dibaca dengan docker history, jadi siapa pun yang bisa menarik image bisa membaca kuncinya. Anggap sudah bocor: rotasi kuncinya, lalu pindah ke RUN --mount=type=secret untuk build atau pengiriman runtime untuk aplikasi.
Mengubah nilai NEXT_PUBLIC di server
Anda mengedit .env dan me-restart container, tetapi browser masih menampilkan nilai lama. Next.js sudah menyisipkannya ke JavaScript saat next build, jadi aplikasi yang berjalan tidak lagi merespons perubahannya. Bangun ulang image dengan nilai baru, atau layani nilainya lewat API.
Secret yang tetap tercetak di log
Penyamaran GitHub tidak dijamin: secret yang di-encode atau diubah bentuknya tidak lagi cocok dengan nilai tersimpan, dan runner hanya bisa menyamarkan secret yang dipakai di job saat ini. Jangan pernah mencetak secret, dan daftarkan setiap nilai sensitif turunannya dengan mask.
Heredoc tanpa tanda kutip yang merusak nilai
Dengan delimiter ENVEOF biasa, bash mengekspansi baris-barisnya, sehingga password yang mengandung tanda dolar atau backtick diam-diam berubah sebelum masuk ke .env, dan aplikasi lalu gagal autentikasi. Beri tanda kutip pada delimiter dan nilai ditulis persis seperti yang dirender GitHub.
Secret yang datang kosong
Workflow yang dipicu dari fork atau oleh Dependabot tidak menerima secret repository, dan variable yang belum diatur adalah string kosong, sehingga step berjalan dengan nilai kosong. Pakai ${VAR:?error} di Compose untuk nilai yang wajib ada, agar nilai kosong gagal dengan jelas saat startup.
| ENV atau ARG di Dockerfile | Secret mount saat build | env_file di Compose | Compose secrets | |
|---|---|---|---|---|
| Kapan ada | ENV: build dan container. ARG: build saja | Satu instruksi build | Di container yang berjalan | Di container yang berjalan |
| Berakhir di image | ENV selalu. ARG terlihat di docker history | Tidak | Tidak | Tidak |
| Bentuk di dalam | Environment variable | File di /run/secrets | Environment variable | File di /run/secrets |
| Cocok untuk secret | Tidak | Ya, untuk kredensial build time | Dokumentasi menyarankan jangan untuk password | Ya, opsi yang didokumentasikan |
Lakukan di komputer Anda sendiri
Anda hanya butuh Docker. Pakai nilai karangan, jangan pernah kunci asli. Anda akan melihat ARG bocor ke docker history, menjaga nilai yang sama tetap di luar dengan secret mount, lalu melihat Compose menyelesaikan variabel yang diatur di dua tempat.
Tulis Dockerfile yang menerima nilai sebagai build argument dan memakainya sekali. Build, lalu cari nilainya di history.
FROM alpine:3 ARG DEMO_TOKEN RUN test -n "$DEMO_TOKEN" # then, in a terminal docker build --build-arg DEMO_TOKEN=not-a-real-secret -t env-lab:arg . docker history --no-trunc env-lab:argSekarang kirim nilai yang sama sebagai build secret. Dockerfile membacanya dari sebuah file hanya untuk satu instruksi, dan history seharusnya tidak lagi menampilkannya.
FROM alpine:3 RUN --mount=type=secret,id=demo test -s /run/secrets/demo # then, in a terminal printf "not-a-real-secret" > demo.txt docker build --secret id=demo,src=demo.txt -t env-lab:secret . docker history --no-trunc env-lab:secretBuat proyek Compose: compose.yaml yang memuat .env, dan .env yang mengatur GREETING. Jalankan container sekali pakai lalu cetak variabelnya.
# compose.yaml services: web: image: alpine:3 env_file: .env # .env GREETING=from-env-file # then, in a terminal docker compose run --rm web printenv GREETINGTambahkan entri environment yang mengatur GREETING lagi, jalankan perintah yang sama, dan lihat nilai mana yang menang. Tebak jawabannya lebih dulu.
services: web: image: alpine:3 env_file: .env environment: GREETING: from-compose-file
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Kenapa password database tidak boleh menjadi ENV di Dockerfile?Lihat jawabannya
Jawaban:Nilai ENV selalu tersimpan di image hasil build dan terbawa ke setiap container, jadi siapa pun yang bisa menarik image bisa membacanya. Kirim saat runtime sebagai gantinya.
2.Kenapa delimiter heredoc di step deploy ditulis 'ENVEOF' dengan tanda kutip?Lihat jawabannya
Jawaban:Bash tidak mengekspansi baris heredoc ketika bagian mana pun dari delimiter diberi tanda kutip. GitHub sudah mengganti nilainya, jadi tanda kutip menjaga tanda dolar di dalam nilai tetap literal.
3.Apa fungsi ${APP_PORT:-3002} di file Compose?Lihat jawabannya
Jawaban:Ia memakai nilai APP_PORT ketika variabel itu diatur dan tidak kosong, dan default setelah :- bila tidak. Workflow menulis fallback yang sepadan, vars.APP_PORT || 3002, ke .env.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.
- Dokumentasi Docker: referensi Dockerfile (ARG dan ENV)
- Dokumentasi Docker: build secrets
- Dokumentasi Docker: mengatur environment variable di Compose
- Dokumentasi Docker: prioritas environment variable
- Dokumentasi Docker: interpolasi Compose
- Dokumentasi Docker: secrets di Compose
- Dokumentasi GitHub: secrets
- Dokumentasi GitHub: security hardening untuk GitHub Actions
- Dokumentasi Next.js: environment variable