ความแตกต่างของ .env, env_file และ environment ใน Docker
ไขข้อข้องใจความแตกต่างระหว่าง .env, env_file และ environment ใน Docker Compose พร้อมลำดับความสำคัญในการใช้งานจริง และเหตุผลที่คุณไม่ควรเก็บรหัสผ่านไว้ในตัวแปรเหล่านี้
สามสิ่งที่ผู้คนมักเรียกว่าไฟล์ env
Docker Compose มีกลไกแยกกัน 3 อย่างที่มีชื่อคล้ายกันจนน่าสับสน ไฟล์ .env จะทำหน้าที่เติมค่าลงในตัวแปร ${VARIABLE} ภายในไฟล์ compose.yaml โดยตรง หลังจากที่ Compose อ่านไฟล์ YAML แล้ว แต่ก่อนที่จะตรวจสอบความถูกต้องหรือรันคำสั่งใดๆ ส่วน attribute env_file: จะโหลดไฟล์ที่เก็บคู่คีย์/ค่าเข้าไปใน environment ของคอนเทนเนอร์ และ attribute environment: จะกำหนดตัวแปรให้กับคอนเทนเนอร์โดยตรงโดยเขียนไว้ในไฟล์ compose ทั้งสามวิธีนี้ไม่สามารถใช้แทนกันได้ และเมื่อมีการกำหนดคีย์เดียวกันจากสองวิธี ลำดับความสำคัญจะเป็นไปตามที่ระบุไว้ในเอกสารอย่างชัดเจน
คู่มือนี้จะแสดงการทำงานของแต่ละวิธี พร้อมพิสูจน์ลำดับความสำคัญด้วยคำสั่งที่คุณสามารถทดลองรันได้ จากนั้นจะครอบคลุมส่วนที่สำคัญกว่า นั่นคือตัวแปร environment นั้นสามารถอ่านได้โดยทุกคนที่สามารถรันคำสั่ง docker inspect ได้ ดังนั้นจึงไม่ควรเก็บรหัสผ่านไว้ในตัวแปรเหล่านี้ หากคุณยังใหม่กับไฟล์ compose โดยทั่วไป ให้เริ่มต้นที่ พื้นฐาน Docker Compose บน VPS แล้วค่อยกลับมาที่นี่เพื่อดูรายละเอียดการตั้งค่า
ไฟล์ .env มีไว้สำหรับไฟล์ compose ไม่ใช่สำหรับคอนเทนเนอร์
สร้างไดเรกทอรีและใส่ไฟล์สองไฟล์ลงไป
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAGตอนนี้ให้สอบถาม Compose ว่าสิ่งที่มันประมวลผลจริงคืออะไร
docker compose configผลลัพธ์ที่ได้แสดง image: alpine:3.20 ตัวแทนค่า (placeholder) หายไปแล้วเนื่องจากเกิดการแทนที่ค่า (interpolation) ในขณะที่ Compose โหลดไฟล์ Compose จะค้นหา .env ในไดเรกทอรีของโปรเจกต์ ซึ่งคือไดเรกทอรีที่ระบุด้วย --project-directory หากคุณตั้งค่าไว้ มิฉะนั้นจะเป็นไดเรกทอรีของไฟล์แรกที่ส่งผ่านด้วย -f หรือหากไม่มีเลยจะเป็นไดเรกทอรีปัจจุบันของคุณ มันจะแทนที่ตัวแทนค่า ${NAME} ที่พบในค่า YAML โดยคีย์จะไม่ถูกแทนที่ค่า และการใช้ $$ ซ้อนกันจะให้ผลลัพธ์เป็นเครื่องหมายดอลลาร์แทนที่จะเป็นการแทนที่ค่า
จากนั้นให้รันเซอร์วิส
docker compose run --rm demoprintenv ALPINE_TAG จะจบการทำงานด้วยสถานะ 1 และไม่แสดงผลลัพธ์ใดๆ ตัวแปรดังกล่าวไม่มีอยู่ภายในคอนเทนเนอร์ นี่คือความเข้าใจผิดที่พบบ่อยที่สุด: .env ใช้สำหรับกำหนดค่าไฟล์ compose ไม่ใช่สำหรับกระบวนการทำงาน (process) ภายใน ไฟล์ .env ที่มี POSTGRES_PASSWORD=hunter2 อยู่ข้างในจะไม่มีผลใดๆ กับฐานข้อมูลของคุณ เว้นแต่ว่าส่วนหนึ่งส่วนใดของไฟล์ compose จะมีการอ้างอิงถึงตัวแปรนั้น
${NAME:-default} ใช้สำหรับกำหนดค่าสำรองเมื่อตัวแปรไม่ได้ถูกตั้งค่าหรือว่างเปล่า ส่วน ${NAME:?message} จะทำให้ Compose ปฏิเสธที่จะเริ่มทำงานและแสดงข้อความของคุณ ซึ่งเป็นทางเลือกที่เหมาะสมสำหรับค่าที่ไม่มีค่าเริ่มต้นที่ปลอดภัย
env_file โหลดตัวแปรเข้าสู่คอนเทนเนอร์
แอตทริบิวต์ env_file: ใช้ระบุไฟล์ตั้งแต่หนึ่งไฟล์ขึ้นไป ซึ่งเนื้อหาภายในไฟล์จะถูกนำไปใช้เป็นตัวแปรสภาพแวดล้อม (environment variables) ของคอนเทนเนอร์
printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.envservices:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.envdocker compose run --rm demoคำสั่งนี้จะแสดงผล from_env_file รูปแบบของไฟล์เป็นข้อความ KEY=value ธรรมดา โดยกำหนดหนึ่งตัวแปรต่อหนึ่งบรรทัด และใช้ # เพื่อเริ่มต้นการเขียนคอมเมนต์ รูปแบบนี้ไม่ใช่ shell แม้ว่า Compose จะประมวลผลด้วยกฎที่คล้ายกับ shell อยู่บ้าง เครื่องหมายคำพูดที่ล้อมรอบค่าจะถูกตัดออก ดังนั้น KEY="value" และ KEY=value จึงให้ผลลัพธ์ที่เหมือนกัน ค่าที่อยู่ในเครื่องหมายคำพูดคู่และค่าที่ไม่มีเครื่องหมายคำพูดจะมีการแทนที่ตัวแปรด้วย ${...} ในขณะที่ค่าที่อยู่ในเครื่องหมายคำพูดเดี่ยวจะถูกนำไปใช้ตามตัวอักษรทั้งหมด คำนำหน้า export สามารถใช้งานได้แต่ไม่จำเป็นต้องระบุ ช่องว่างรอบเครื่องหมาย = จะถูกตัดออก สิ่งที่ต้องระวังคือการเขียนคอมเมนต์แบบแทรกบรรทัด: ในค่าที่ไม่มีเครื่องหมายคำพูด หากมีช่องว่างตามด้วย # จะถือเป็นการเริ่มต้นคอมเมนต์ ดังนั้นควรใส่เครื่องหมายคำพูดให้กับค่าใดก็ตามที่มี #
หากไม่พบพาธของ env_file จะถือเป็นข้อผิดพลาดและ Compose จะหยุดทำงาน ให้ทำเครื่องหมายว่าเป็นตัวเลือกเสริม (optional) หากไฟล์นั้นอาจไม่มีอยู่จริงตามความเหมาะสม:
env_file:
- path: ./app.env
required: falseการตั้งค่า environment variable แบบ inline
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentDocker Compose รองรับไวยากรณ์ 2 รูปแบบ คือรูปแบบ mapping ที่แสดงด้านบน และรูปแบบรายการโดยใช้ - GREETING=from_environment ทั้งสองรูปแบบทำงานเหมือนกันทุกประการ และมีเทคนิคเพิ่มเติมอีกหนึ่งอย่างคือ หากระบุ key โดยไม่มีค่า ระบบจะดึงค่าตัวแปรมาจาก shell ที่คุณรันคำสั่ง docker compose โดยตรง ในรูปแบบรายการจะใช้เพียง - GREETING ส่วนในรูปแบบ mapping จะใช้ GREETING: โดยเว้นว่างหลังเครื่องหมาย colon ไว้
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoคำสั่งดังกล่าวจะพิมพ์ค่า from_my_shell ออกมา หากคุณรันคำสั่งโดยไม่ได้ตั้งค่า GREETING ใน shell ระบบ Compose จะไม่ตั้งค่าใดๆ และไม่มีการแจ้งเตือนใดๆ ทั้งสิ้น คุณควรทราบถึงกรณีที่การส่งผ่านค่าล้มเหลวโดยเงียบเชียบเช่นนี้ เพราะหาก service เริ่มทำงานด้วยตัวแปร password ที่ว่างเปล่า มักจะทำให้ service นั้นเริ่มทำงานได้สำเร็จแต่กลับเปิดช่องโหว่ให้เข้าถึงได้โดยไม่มีการป้องกัน
ลำดับความสำคัญของค่าคอนฟิก
Docker ได้ระบุลำดับความสำคัญของค่าคอนฟิกไว้ โดยเรียงจากสูงสุดไปต่ำสุดดังนี้: docker compose run -e บนบรรทัดคำสั่ง, ตามด้วย environment หรือ env_file ซึ่งค่าจะถูกดึงมาจากเชลล์ของคุณหรือจากไฟล์ env, จากนั้นคือ environment แบบปกติในไฟล์ compose, ตามด้วย env_file และสุดท้ายคือคำสั่ง ENV ที่ถูกฝังไว้ในอิมเมจ
สรุปสั้นๆ สำหรับการใช้งานทั่วไป: environment: มีลำดับความสำคัญสูงกว่า env_file: และ -e บนบรรทัดคำสั่งจะมีลำดับความสำคัญสูงกว่าทั้งสองอย่าง คุณสามารถพิสูจน์ได้ด้วยไฟล์เดียว
services:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.env
environment:
GREETING: from_environmentdocker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETINGผลลัพธ์แรกจะแสดงค่า from_environment ซึ่งหมายความว่า environment: ได้เขียนทับค่าใน app.env ไปแล้ว ส่วนผลลัพธ์ที่สองจะแสดงค่า from_cli โดยไม่มีค่าใดในไฟล์ compose ที่สามารถเขียนทับบรรทัดคำสั่งได้
เมื่อคอนเทนเนอร์ทำงานโดยไม่สนใจคอนฟิกที่คุณตั้งค่าไว้ อย่าใช้วิธีเดาสุ่ม ให้ใช้ docker compose config เพื่อแสดงไฟล์ที่ประมวลผลเสร็จสมบูรณ์แล้ว และใช้ docker compose config --environment เพื่อแสดงตัวแปร interpolation ที่ Compose กำลังใช้งานอยู่ รายงานปัญหาประเภท "ไฟล์ env ของฉันถูกละเลย" ส่วนใหญ่เกิดจากการตั้งค่าซ้ำซ้อนในระดับที่ต่างกัน
เหตุใดตัวแปรสภาพแวดล้อมจึงรั่วไหล
การตั้งรหัสผ่านใน environment: จะทำให้รหัสผ่านถูกจัดเก็บไว้ในไฟล์คอนฟิกูเรชันของคอนเทนเนอร์บนดิสก์ ซึ่งผู้ใช้ทุกคนในกลุ่ม docker สามารถมองเห็นได้
docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'ผลลัพธ์ที่ได้จะมี "DB_PASSWORD=hunter2" ปรากฏเป็นข้อความธรรมดา นอกจากนี้ยังมีอีก 3 พาธที่เปิดเผยค่าเดียวกันนี้ docker compose config จะแสดงค่าดังกล่าวออกมาที่เทอร์มินัล ซึ่งเป็นสาเหตุที่ทำให้ค่านี้ถูกคัดลอกไปวางในฟอรัมสนับสนุน กระบวนการลูก (child process) จะสืบทอดตัวแปรเหล่านี้โดยปริยาย เว้นแต่กระบวนการแม่จะตั้งใจเริ่มกระบวนการลูกด้วยสภาพแวดล้อมใหม่ และกระบวนการที่รันด้วยผู้ใช้เดียวกับ PID 1 หรือถือสิทธิ์ CAP_SYS_PTRACE จะสามารถอ่านค่านี้ได้จาก /proc/1/environ ในคอนเทนเนอร์ที่ทุกอย่างรันด้วย root ทุกกระบวนการจึงเข้าถึงข้อมูลนี้ได้ สำหรับแอปพลิเคชันที่รันด้วยผู้ใช้ที่ไม่มีสิทธิ์ในขณะที่ PID 1 รันด้วย root จะได้รับข้อผิดพลาดเรื่องสิทธิ์ในการเข้าถึงไฟล์ดังกล่าว แม้ว่าตัวแอปพลิเคชันเองจะยังคงถือตัวแปรนั้นไว้ในสภาพแวดล้อมของตนก็ตาม และตัวจัดการข้อผิดพลาดของแอปพลิเคชันมักจะบันทึกสภาพแวดล้อมทั้งหมดลงใน log หรือรายงานข้อผิดพลาดโดยอัตโนมัติ
การเป็นสมาชิกของกลุ่ม docker เทียบเท่ากับการมีสิทธิ์ root บนโฮสต์ ดังนั้นนี่จึงไม่ใช่ขอบเขตการจำกัดสิทธิ์ที่คุณควรพึ่งพา คู่มือเรื่อง บัญชีผู้ใช้ที่มีสิทธิ์น้อยที่สุดบน VPS ได้อธิบายเหตุผลว่าทำไมจึงควรจำกัดการเข้าถึงกลุ่มดังกล่าวบนเซิร์ฟเวอร์ที่ใช้งานร่วมกันทุกเครื่อง
การเก็บค่าความลับใน Compose ด้วยไฟล์
Compose รองรับการใช้งานความลับ (secrets) ในรูปแบบไฟล์ โดยค่าความลับจะถูก mount เข้าไปในคอนเทนเนอร์เป็นไฟล์ แทนที่จะถูกฉีดเข้าไปใน environment variable
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtไฟล์ความลับจะถูก mount ไว้ที่ /run/secrets/db_password ภายในคอนเทนเนอร์ ชื่อที่อยู่หลังเครื่องหมาย slash คือชื่อของความลับที่กำหนดไว้ในบล็อก secrets: ระดับบนสุด
ส่วนต่อท้าย _FILE เป็นข้อตกลงที่ Docker Official Images ใช้กัน รวมถึง postgres, mysql และ mariadb โดยสคริปต์ entrypoint เหล่านั้นจะตรวจสอบหา VARNAME_FILE, อ่านไฟล์ และนำเนื้อหาไปใช้งาน นี่ไม่ใช่ฟีเจอร์ของ Docker โดยตรง จึงจะใช้งานได้เฉพาะกับอิมเมจที่รองรับเท่านั้น โปรดตรวจสอบเอกสารประกอบของอิมเมจก่อนใช้งานเพื่อให้แน่ใจว่า SOMETHING_FILE จะทำงานตามที่คาดหวัง สำหรับแอปพลิเคชันที่ไม่รองรับ คุณมักจะสามารถเขียนโปรแกรมให้อ่านไฟล์ดังกล่าวเองเมื่อเริ่มทำงาน หรือส่งผ่าน path ของไฟล์เพื่อให้ entrypoint ของคุณจัดการเองได้
ตรวจสอบจากภายในคอนเทนเนอร์ที่กำลังทำงานอยู่:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORDคำสั่งแรกจะแสดงรหัสผ่านออกมา ส่วนคำสั่งที่สองจะไม่แสดงผลใดๆ เนื่องจากค่าดังกล่าวไม่เคยถูกนำเข้าสู่ environment นี่คือจุดประสงค์หลักของการใช้งาน: docker inspect บนคอนเทนเนอร์นี้จะแสดงเฉพาะ path ที่ไม่เป็นอันตรายเท่านั้น
โปรดปกป้องไฟล์ต้นฉบับบนโฮสต์ เพราะความปลอดภัยของความลับขึ้นอยู่กับระดับการเข้าถึงไฟล์นั้น:
chmod 600 db_password.txtทางสายกลางที่ใช้งานได้จริงบน VPS
อิมเมจสำหรับ self-host หลายรายการไม่รองรับตัวแปร _FILE ดังนั้น environment variables จึงเป็นวิธีเดียวในการตั้งค่า บน VPS ที่มีผู้ดูแลระบบคนเดียว เป้าหมายที่ทำได้จริงคือการป้องกันไม่ให้ค่าเหล่านี้อยู่ในไฟล์ที่ทุกคนอ่านได้ในไดเรกทอรีโปรเจกต์ และป้องกันไม่ให้ค่าเหล่านี้หลุดเข้าไปใน git
sudo install -o root -g root -m 600 /dev/null /etc/myapp/app.env
sudo nano /etc/myapp/app.env env_file:
- /etc/myapp/app.envinstall -m 600 จะสร้างไฟล์โดยกำหนดสิทธิ์มาให้ตั้งแต่ต้น จึงไม่มีช่วงเวลาที่ไฟล์จะถูกเปิดให้ผู้อื่นอ่านได้ ไฟล์นี้มี root เป็นเจ้าของ ดังนั้นผู้ใช้ที่ไม่ใช่ root บนเครื่องจึงไม่สามารถอ่านได้ แม้ว่าใครก็ตามที่สามารถรัน docker จะยังคงอ่านค่าจากภายในคอนเทนเนอร์ได้ก็ตาม ให้เพิ่ม *.env และ .env ลงใน .gitignore แล้ว commit ไฟล์ app.env.example ที่ระบุชื่อคีย์โดยเว้นค่าว่างไว้แทน รหัสผ่านที่ถูก commit ไปแล้วถือว่าเป็นรหัสผ่านที่ต้องเปลี่ยนใหม่
การเปลี่ยนค่า (rotate) หมายถึงการต้องรีสตาร์ทเซอร์วิส เนื่องจาก environment variables จะถูกอ่านเพียงครั้งเดียวเมื่อกระบวนการของคอนเทนเนอร์เริ่มทำงาน การแก้ไขไฟล์จึงไม่มีผลใดๆ จนกว่าคุณจะรัน docker compose up -d --force-recreate db นี่เป็นรูปแบบเดียวกับที่ใช้ในคู่มือ n8n หลัง HTTPS บน VPS ซึ่งคีย์การเข้ารหัสจะถูกเก็บไว้นอกไฟล์ compose
การแยกไฟล์ตั้งค่าตามสภาพแวดล้อม
โดยปกติแล้ว Compose จะอ่าน .env จากไดเรกทอรีของโปรเจกต์ คุณสามารถระบุตำแหน่งอื่นได้โดยใช้ --env-file
docker compose --env-file .env.staging configระบบจะอ่านไฟล์หลายไฟล์ตามลำดับ โดยไฟล์ที่ระบุภายหลังจะเขียนทับค่าในไฟล์ก่อนหน้า ให้เก็บค่าเริ่มต้นที่ไม่เป็นความลับไว้ในไฟล์ที่คอมมิตลงระบบควบคุมเวอร์ชัน และเก็บข้อมูลลับไว้ในไฟล์ที่ต้องไม่ถูกนำออกจากเซิร์ฟเวอร์ หลักการเดียวกันนี้ใช้กับ env_file: ซึ่งไฟล์ที่ระบุไว้ลำดับสุดท้ายจะมีผลเหนือกว่าหากมีคีย์ที่ซ้ำกัน
FAQ
ทำไมไฟล์ .env ของฉันถึงถูกละเลยภายในคอนเทนเนอร์?
ไฟล์ไม่ได้ถูกละเลย .env จะทำหน้าที่แทนที่ตัวแปรใน ${NAME} ภายในไฟล์ compose และยังสามารถกำหนดค่าตัวแปรของ Compose เองได้ เช่น COMPOSE_PROJECT_NAME หรือ COMPOSE_FILE แต่จะไม่กำหนดค่าตัวแปรภายในคอนเทนเนอร์โดยตรง หากต้องการส่งค่าเข้าไปในคอนเทนเนอร์ ให้ทำการอ้างอิงค่าดังกล่าวในรูปแบบ environment: { KEY: "${NAME}" } หรือใช้ env_file: ./that-file.env แทน
ระหว่าง environment กับ env_file อย่างไหนมีความสำคัญสูงกว่ากัน?
environment: จะมีความสำคัญสูงกว่า ตามเอกสารของ Docker ลำดับความสำคัญของแอตทริบิวต์ environment จะอยู่เหนือ env_file และทั้งสองอย่างจะอยู่ต่ำกว่า docker compose run -e ที่ระบุผ่านบรรทัดคำสั่ง หากมีการกำหนดค่าคีย์เดียวกันไว้ในทั้งสองที่ ค่าที่อยู่ใน env_file จะถูกละเลยไปโดยไม่มีการแจ้งเตือน
ฉันจะดูค่าสุดท้ายที่ Compose ใช้งานได้อย่างไร?
ให้รันคำสั่ง docker compose config เพื่อแสดงไฟล์ compose ที่ผ่านการแทนที่ตัวแปรทั้งหมดแล้ว สำหรับคอนเทนเนอร์ที่กำลังทำงานอยู่ docker inspect <container> --format '{{json .Config.Env}}' จะแสดงค่าที่กระบวนการ (process) ได้รับจริง
Secrets ใน Compose มีการเข้ารหัสหรือไม่?
ไม่มี ไฟล์ secret จะถูก mount เข้าไปในคอนเทนเนอร์เป็นไฟล์ธรรมดาที่ตำแหน่ง /run/secrets/<name> และไฟล์ต้นฉบับบนดิสก์ของโฮสต์ก็ไม่ได้ถูกเข้ารหัส ประโยชน์ของมันคือการจำกัดขอบเขต (scope) ไม่ใช่การเข้ารหัส โดยค่าดังกล่าวจะไม่หลุดเข้าไปอยู่ใน environment ของคอนเทนเนอร์, ไม่ปรากฏในผลลัพธ์ของ docker inspect และไม่ติดไปกับ crash dump ที่พิมพ์ค่า environment ออกมา
ฉันสามารถใช้เครื่องหมายคำพูดและเว้นวรรคในไฟล์ env ได้หรือไม่?
ได้ Compose จะตัดเครื่องหมายคำพูดที่ล้อมรอบออก ดังนั้นทั้ง KEY="value with spaces" และ KEY=value with spaces จะให้ค่าเป็น value with spaces เหมือนกัน ให้ใช้เครื่องหมายคำพูดเดี่ยวเมื่อค่ามี $ ที่ไม่ต้องการให้มีการแทนที่ตัวแปร และให้ใส่เครื่องหมายคำพูดสำหรับค่าที่มี # เพราะหากไม่ใส่เครื่องหมายคำพูด ตัวอักษรดังกล่าวจะถูกมองว่าเป็นจุดเริ่มต้นของคอมเมนต์ ส่วนช่องว่างรอบ = จะถูกตัดออก ดังนั้น KEY = value จึงยังคงกำหนดค่า KEY ได้ตามปกติ