ความแตกต่างของ .env, env_file และ environment ใน Docker
ไขข้อข้องใจความแตกต่างระหว่าง .env, env_file และ environment ใน Docker Compose พร้อมลำดับความสำคัญในการใช้งานจริง และเหตุผลที่คุณไม่ควรเก็บรหัสผ่านไว้ในตัวแปรเหล่านี้
สามสิ่งที่ผู้คนมักเรียกว่าไฟล์ env
Docker Compose มีกลไกแยกกัน 3 อย่างที่มีชื่อคล้ายกันจนน่าสับสน ไฟล์ .env จะทำหน้าที่เติมค่าลงในตัวแปร ${VARIABLE} ภายในไฟล์ compose.yaml เอง ก่อนที่ Compose จะเริ่มประมวลผลไฟล์นั้น ส่วนแอตทริบิวต์ env_file: จะโหลดไฟล์ที่ประกอบด้วยคู่คีย์/ค่าเข้าไปใน environment ของคอนเทนเนอร์ และแอตทริบิวต์ environment: จะกำหนดค่าตัวแปรให้กับคอนเทนเนอร์โดยตรงโดยเขียนไว้ในไฟล์ compose ทั้งสามวิธีนี้ไม่สามารถใช้แทนกันได้ และเมื่อมีการกำหนดคีย์เดียวกันจากสองวิธี ลำดับความสำคัญจะเป็นไปตามที่ระบุไว้ในเอกสารอย่างชัดเจน
คู่มือนี้จะแสดงวิธีการทำงานของแต่ละส่วน พร้อมพิสูจน์ลำดับความสำคัญด้วยคำสั่งที่คุณสามารถทดลองรันได้จริง จากนั้นจะครอบคลุมส่วนที่สำคัญกว่า นั่นคือตัวแปร environment สามารถอ่านได้โดยทุกคนที่สามารถรันคำสั่ง docker inspect ได้ ดังนั้นจึงไม่ควรเก็บรหัสผ่านไว้ในตัวแปรเหล่านี้ หากคุณยังใหม่กับไฟล์ compose โดยทั่วไป ให้เริ่มต้นที่ พื้นฐาน Docker Compose บน VPS แล้วจึงกลับมาที่นี่เพื่อดูรายละเอียดการตั้งค่า
ไฟล์ .env มีไว้สำหรับไฟล์ compose ไม่ใช่สำหรับ container
สร้างไดเรกทอรีและใส่ไฟล์สองไฟล์ลงไป
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 จะมองหา .env ในไดเรกทอรีของโปรเจกต์ ซึ่งเป็นไดเรกทอรีที่เก็บไฟล์ compose ไว้ และจะแทนที่ทุก ${NAME} ที่พบ
จากนั้นให้รัน service
docker compose run --rm demoprintenv ALPINE_TAG จบการทำงานด้วยสถานะ 1 และไม่แสดงผลลัพธ์ใด ๆ ตัวแปรดังกล่าวไม่มีอยู่ภายใน container นี่คือความเข้าใจผิดที่พบบ่อยที่สุด: .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 ในกรณีส่วนใหญ่ เครื่องหมายคำพูดจะถูกเก็บไว้เป็นส่วนหนึ่งของค่า และไม่จำเป็นต้องใช้คำนำหน้า export ห้ามเว้นวรรคหน้าหรือหลังเครื่องหมาย = เพราะ KEY = value จะทำให้เกิดตัวแปรที่ชื่อ KEY โดยมีช่องว่างนำหน้าในค่าของตัวแปรนั้น
หากไม่พบไฟล์ตามพาธ env_file จะถือเป็นข้อผิดพลาดและ Compose จะหยุดทำงาน ให้ทำเครื่องหมายว่าเป็นตัวเลือกเสริม (optional) หากไฟล์นั้นอาจไม่มีอยู่จริงตามความเหมาะสม:
env_file:
- path: ./app.env
required: falseการตั้งค่าตัวแปร environment แบบ inline
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentDocker Compose ยอมรับรูปแบบไวยากรณ์ 2 แบบ คือรูปแบบการจับคู่ (mapping) ด้านบน และรูปแบบรายการ (list) ที่ใช้ - GREETING=from_environment ทั้งสองรูปแบบทำงานเหมือนกันทุกประการ แต่รูปแบบรายการมีลูกเล่นเพิ่มเติมคือ หากระบุเฉพาะชื่อตัวแปรโดยไม่มีค่า ระบบจะส่งผ่านค่าตัวแปรนั้นมาจาก shell ที่คุณรันคำสั่ง docker compose
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoคำสั่งดังกล่าวจะแสดงผลเป็น from_my_shell หากคุณรันคำสั่งโดยไม่ได้ตั้งค่า GREETING ไว้ใน shell ระบบ Compose จะไม่ตั้งค่าใดๆ และไม่มีการแจ้งเตือนใดๆ ทั้งสิ้น คุณควรทราบว่าการส่งผ่านค่าที่ล้มเหลวโดยเงียบเชียบนี้เป็นเรื่องสำคัญ เพราะบริการที่เริ่มทำงานด้วยตัวแปร password ว่างเปล่ามักจะเริ่มทำงานได้สำเร็จ แต่จะเปิดช่องโหว่ให้เข้าถึงได้โดยไม่มีการป้องกัน
ลำดับความสำคัญของค่าคอนฟิก
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 จะพิมพ์ค่าดังกล่าวออกมาที่เทอร์มินัล ซึ่งเป็นสาเหตุที่ทำให้ค่านี้ถูกคัดลอกไปวางในฟอรัมสนับสนุน กระบวนการใดๆ ภายในคอนเทนเนอร์สามารถอ่าน /proc/1/environ ได้ และทุกกระบวนการลูกจะได้รับสืบทอดตัวแปรนี้ไปโดยปริยาย อีกทั้งตัวจัดการเมื่อแอปพลิเคชันขัดข้องมักจะดัมพ์สภาพแวดล้อมทั้งหมดลงในบันทึกหรือรายงานข้อผิดพลาดเป็นประจำ
การเป็นสมาชิกของกลุ่ม 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 เท่านั้น มันไม่ได้ตั้งค่าตัวแปรภายในคอนเทนเนอร์โดยตรง หากต้องการนำค่าเข้าไปในคอนเทนเนอร์ ให้ใช้อ้างอิงผ่าน 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}}' เพื่อดูว่ากระบวนการทำงานได้รับค่าใดไปใช้งานจริง
Compose secrets มีการเข้ารหัสหรือไม่?
ไม่มี ไฟล์ secret ที่อ้างอิงจากไฟล์จะถูก mount เข้าไปในคอนเทนเนอร์เป็นไฟล์ธรรมดาที่ /run/secrets/<name> และไฟล์ต้นทางบนดิสก์ของโฮสต์ก็ไม่ได้ถูกเข้ารหัส ประโยชน์ของมันคือการจำกัดขอบเขต ไม่ใช่การเข้ารหัส โดยค่าดังกล่าวจะไม่อยู่ในสภาพแวดล้อมของคอนเทนเนอร์ ไม่อยู่ในผลลัพธ์ของ docker inspect และไม่อยู่ใน crash dump ที่พิมพ์ค่า environment ออกมา
ฉันสามารถใช้เครื่องหมายคำพูดและเว้นวรรคในไฟล์ env ได้หรือไม่?
ให้ใช้ KEY=value with spaces และไม่ต้องใส่เครื่องหมายคำพูด Compose จะถือว่าข้อความที่เหลือทั้งหมดของบรรทัดนั้นเป็นค่าของตัวแปร ดังนั้นเครื่องหมายคำพูดมักจะกลายเป็นตัวอักษรที่ติดไปกับค่าจริง ห้ามเว้นวรรคหน้าหรือหลัง = เพราะจะทำให้คีย์มีช่องว่างติดไปด้วยและไม่มีอะไรจับคู่กับมันได้