Docker (LV.1)
Docker Basic
Requirement
ต้องมีพื้นฐานความรู้ Linux เบื้องต้น เช่น shell script, file & folder, permission, Linux package management เป็นต้น ก่อนเรียนเนื้อหาส่วนนี้
Docker คืออะไร
Docker เป็น Platform ที่ช่วยเราในด้านการพัฒนา ส่งมอบ และรันแอพพลิเคชั่น Docker จะช่วยให้เราแพ็คและรันแอพพลิเคชั่น ภายใต้ environment ที่เรียกว่า container ซึ่งจะไม่ส่งผลกระทบต่อ container อื่นๆได้ ซึ่งนั่นจะทำให้เราสามารถมีหลายๆ container ทำงานพร้อมกันภายใต้ host เดียวกันได้
ข้อแตกต่างระหว่าง Docker และ VM
Virtual Machine (VM) เป็นการจำลอง hardware ของ host ให้กับ guest VM ทำให้เราสามารถมีเครื่องในเชิง logical หลายๆเครื่องได้บน physical server เดียว แต่การจะสร้าง VM ได้ เราต้องมี software ที่ทำงานในการจำลอง hardware ซึ่งเรียกว่า Hypervisor และหลังจากจำลอง hardware แล้ว เราจะต้องติดตั้ง Operating System (OS) ลงบน guest VM นั้นอีก แล้วจึงนำ library (lib) และแอพพลิเคชั่นมาติดตั้ง
Docker ทำงานโดยมี Docker Engine ทำงานอยู่บน Host OS และ container ที่ประกอบด้วย lib และแอพพลิเคชั่น มารันงานอยู่บน Docker Engine การทำงานของ Container บน Docker จึงเหมือน process บน OS มากกว่าการเป็น VM
Virtual Machine
Docker
Heavyweight
Lightweight
แต่ละ VM จะมี OS ของตัวเอง
แต่ละ container ใช้งาน kernel ของ host ร่วมกัน
ใช้เวลาในการเริ่มทำงานของแอพพลิเคชั่นในระดับนาที
ใช้เวลาในการเริ่มทำงานของแอพพลิเคชั่นในระดับมิลลิวินาที
ใช้ Memory เยอะกว่า เนื่องจากเสีย overhead ให้กับ Hypervisor และ Guest VM
ใช้ Memory น้อยกว่า ทำให้ Container ได้ Memory ไปเกือบทั้งหมด
ใช้พื้นที่ Disk เยอะกว่า เสีย overhead ซ้ำซ้อนให้แต่ละ guest OS
ใช้พื้นที่ Disk น้อยกว่า มีการเก็บข้อมูลเป็น layer และจะไม่เก็บส่วนที่ซ้ำกัน
มีการแยกจากกันชัดเจน
แยกกันในระดับ process
ประโยชน์จากการใช้ Docker
ช่วยให้การพัฒนาแอพพลิเคชั่นเป็นไปอย่างรวดเร็ว
ความสามารถในการย้ายข้ามเครื่อง
version control และการใช้ซ้ำ
การแชร์
lightweight และมี overhead น้อย
ทำให้การดูแลง่ายขึ้น
การแสดงข้อมูลทั่วไปของ Docker
command จะแสดง version ของ Docker, containerd และ runc ที่ติดตั้ง
จะแสดงข้อมูลทั่วไปของระบบ เช่น จำนวน container ในระบบ จำนวน container image หรือแม้กระทั่งจำนวน cpu, memory และประเภท cpu architecture ของระบบ
เริ่มต้นใช้งาน Container
การรันคำสั่ง docker run คือการนำ Docker Image มาสร้างเป็น container และเริ่มทำงาน
จะพบว่าหากลองเข้าเว็บ site ด้วย ip ของเครื่องที่รัน container นี้อยู่ จะแสดงหน้าเว็บ nginx ขึ้นมา
แต่จากตัวอย่างข้างต้น container ที่สร้างขึ้นมาจะทำงานเป็น foreground mode ทำให้ไม่มีการ return prompt กลับมา และหากเราสั่ง Ctrl+C
เพื่อออก หรือรันผ่าน ssh แล้วมีการ disconnect ไป ก็จะทำให้ container ถูก kill ไปด้วย
ให้ออก แล้วรันด้วยคำสั่งใหม่ดังนี้
การเพิ่ม option -d
ลงไปทำให้ container รันใน detach mode และจะ return prompt กลับมาให้เรา
หากมีการรันผ่าน ssh เวลา disconnect ไป container ก็ยังทำงานอยู่ตามปกติ
ข้อควรระวัง option ต่างๆ ต้องใส่อยู่ก่อนชื่อ container image เพราะ syntax ใดๆที่ตามหลังชื่อ container image นั้นจะเป็นส่วนของ command ที่ pass ให้กับ container ทั้งหมด
เราสามารถดูได้ว่ามี container ใดรันอยู่บ้างในระบบได้ด้วยคำสั่งดังนี้
คำสั่งจะแสดง container ที่ทำงานอยู่เท่านั้น
การเพิ่ม option -a
หลังคำสั่ง docker ps
จะทำให้แสดง container ทั้งหมด รวมถึง container ที่ไม่ได้ทำงานอยู่ด้วย
คำสั่ง docker container ls
และ docker container ls -a
จะให้ผลเหมือนกับ docker ps
และ docker ps -a
ตามลำดับ
คำสั่ง docker container stop
ตามด้วยชื่อ container หรือ container id นั้นๆ จะใช้หยุดการทำงานของ container และคำสั่ง docker container start
ตามด้วยชื่อ container หรือ container id นั้นจะทำให้ container กลับมาทำงานใหม่
คำสั่ง docker container rm
ตามด้วยชื่อ container หรือ container id นั้น จะลบ container นั้นทิ้ง
docker container rm
จะลบได้เฉพาะ container ที่ไม่ได้ทำงานอยู่เท่านั้น
หากจะลบ container ที่ทำงานอยู่ ต้องใส่ option -f
เพิ่มไปด้วย เพื่อ force ลบ container ทิ้ง
ข้อควรระวัง การใส่ option -f เสมอเวลาทำการลบ container ต่างๆ เป็นสิ่งที่ไม่แนะนำ เพราะจะเกิดเป็นความเคยชิน และส่งผลเสียได้ หากลบ container ผิดตัวโดยไม่ได้ตั้งใจ
Container เป็นเพียง process บนเครื่อง
คำสั่งข้างต้นจะทำการสร้าง container ซึ่งรัน MongoDB ขึ้นมา และ list process ในระบบที่มีคำว่า mongod
หยุดการทำงานของ container ชื่อ mongo ที่สร้างไว้จากคำสั่งก่อนหน้า และลอง list process อีกครั้ง จะพบว่าบน host นั้น ไม่มี process ชื่อ mongod อยู่แล้ว
ที่เป็นเช่นนั้นเพราะ container เป็นเพียง process หนึ่งที่ทำงานอยู่บน host เท่านั้น และมี isolation เพียงระดับ container ด้วยกันเองเท่านั้น ดังนั้นจากบน host จึงยังเห็น process ที่ทำงานอยู่ภายในแต่ละ container อยู่ จะต่างจาก VM ที่ process ภายใน VM guest กับ process บน Host แยกออกจากกันชัดเจน
การที่บน host เห็น process ของภายในแต่ละ container ไม่ถือเป็นเรื่องไม่ปลอดภัย เพราะในการใช้งานร่วมกันหลาย user บน production นั้น เราจะออกแบบระบบแยกแต่ละ user ไม่ได้ให้ใครเข้าถึง root ได้ และแต่ละ user จะถูก limit สิทธิ์ไว้ให้สร้าง container ของตนเองเท่านั้น เมื่อแต่ละ container ถูกจำกัดสิทธิ์ไม่สามารถเข้าถึงข้อมูลของ container อื่นได้โดยตรง ก็จะทำให้ระบบปลอดภัย
Cleanup
ขั้นตอนที่เกิดขึ้นเมื่อสั่ง docker run
docker run
engine ตรวจสอบว่ามี container image ที่เรียกใช้นั้น อยู่ภายใน image cache ของระบบหรือไม่
หากไม่พบ image ภายใน image cache ก็จะไปหาจาก remote image repository ซึ่งโดย default นั้นจะเป็น Docker Hub
หากเจอ image บน remote image repository ก็จะ download ลงมาเก็บที่ image cache ในกรณีที่ไม่ได้ระบุ tag ของ image จะถือว่าใช้ tag
latest
สร้าง container จาก container image นั้น และเตรียมเริ่มทำงาน
allocate virtual ip จาก Docker network
หากมีการใส่ option
-p
ให้ publish port ไว้ จะเปิด port บน host และทำ forward ไปยัง port ภายใน containercontainer เริ่มทำงานตาม
CMD
ที่กำหนดไว้
การ forward port ของ Docker จะใช้ iptables ดังนั้นหากมีการปิด service iptables ไว้ จะทำให้ network ของ Docker มีปัญหา
หาก Server มีการทำ Hardening ส่วน sysctl ต้องระวัง ไม่ให้มีการไป disable การ forward packet
net.ipv4.ip_forward = 1 net.ipv4.conf.all.forwarding = 1
การสั่ง docker run พร้อม option -p
นั้น ตามขั้นตอนจะมีการสร้าง container ขึ้นมาก่อน แล้วจึงค่อยทำ forward port
ดังนั้นหากมีการ publish port ชนกับ port ที่ host ใช้งานอยู่ ตัว container ที่สั่งสร้างขึ้นมาก็ยังคงอยู่เช่นเดิม แต่ส่วนที่ forward port เท่านั้นที่มีปัญหา
ดังนั้นเวลาเกิดกรณีเช่นนั้น อย่าลืมลบ container ก่อนสั่งสร้างใหม่อีกครั้งด้วย
Docker Hub
Docker Hub เป็น Default remote repository ของ Docker engine เมื่อเราติดตั้งมา ดังนั้นคำสั่ง docker run หรือ docker image pull ต่างๆ ที่อ้างอิงถึง image ซึ่งเราไม่ได้ระบุชื่อ repository ก็จะอ้างอิงถึง remote repository จาก Docker Hub
ภายในจะประกอบด้วย Repository ต่างๆ แบ่งประเภทหลักๆ ได้ดังนี้
Official Images Repository - คือ Repository ที่เก็บ Image ซึ่ง publish มาโดยทาง Docker เอง
Verified Publisher Repository - คือ Repository ที่เก็บ Image จากองค์กรที่เชื่อถือได้ เช่น mysql ของบริษัท Oracle
Individual Repository - คือ Repository ของผู้ใช้ทั่วไป ที่บางส่วนก็เปิดเป็น public repository ไว้ ให้คนอื่นสามารถนำ Docker Image ของตนไปใช้ได้
Official Image จะสามารถ pull ด้วชื่อ Image ได้เลย ไม่ต้องใส่ชื่อ account นำหน้า เช่น nginx, mysql, redis Verified Publisher และ Individual จะมีชื่อ account ขององค์กรนำหน้าและ slash ตามด้วยชื่อ Image เช่น bitnami/redis, amazon/aws-cli
การหาชื่อ Docker Image ที่ใช้ในการ pull
การเลือก tag ของ Docker Image
ถ้าไม่กำหนด tag เวลา pull จะใช้ tag latest เสมอ ซึ่งในบางครั้งเราอาจจะไม่ต้องการเช่นนั้น เพราะ tag latest อาจจะไม่ใช้ stable version เสมอไป
Repository ที่ดีมักจะมีเขียนในส่วน Document ไว้อยู่ว่า ปัจจุบัน tag image ใดบ้างที่ตรงกัน เช่น tag 1.21.3 ก็จะตรงกับ tag mainline, tag 1.21 และ tag latest tag 1.20.1 ก็จะตรงกับ tag stable, tag 1.20 และจะมี link ที่โยงไปถึง Dockerfile ว่า แต่ละ Image ใช้คำสั่งใดบ้างในการสร้างขึ้นมา
ในที่นี้ หากเราต้องการ maintain ระบบให้เป็น stable ล่าสุดเสมอ ก็อาจจะใช้ tag stable แทน แต่หากเราต้องการทดสอบ software version ใหม่ล่าสุดเสมอ ก็อาจจะใช้ tag latest หากระบบใดที่เป็น production ก็อาจจะต้องการ maintain tag ซึ่ง freeze เลข major กับ minor version แต่ให้ patch version เปลี่ยนได้เรื่อยๆ ก็อาจจะใช้ tag 1.20 หากระบบใดต้องการลดความเสี่ยงจากการ update patch ก็อาจจะ freeze ไปถึงเลข patch version ก็จะใช้ tag 1.20.1 ก็เป็นได้ ขึ้นกับ policy แต่ละองค์กร
ในบางครั้งเราอาจจะพบว่า tag ปกติมีขนาดใหญ่มาก เพราะ build มาจาก Base Image ที่ใหญ่ จะมีบาง software ที่ใช้ tag ซึ่งมีคำว่า slim หรือ alpine ด้วย ซึ่ง Image ประเภทนี้จะมีการใช้ Base Image ซึ่งตัด binary และ lib หลายตัวออกไป ทำให้ขนาด Image เล็กลง
ข้อแตกต่างระหว่าง Slim และ Alpine
tag alpine เป็น Base Image ที่มี OS เป็น Alpine Linux และจะแทบไม่ได้ติดตั้ง binary หรือ lib อะไรมาเลย ดังนั้นจะเล็กมาก
tag slim เกิดขึ้นมาทีหลัง tag alpine และต่างจาก tag alpine เพราะไม่ได้ใช้ OS Alpine Linux แต่จะเป็น Base OS แบบ Linux ตามทั่วไปอย่าง CentOS หรือ Debian ซึ่งนิยมใช้ในการติดตั้ง Software มาตั้งแต่สมัยก่อนแล้ว แต่มีการตัด binary และ lib ส่วนใหญ่จาก Base OS ของ CentOS หรือ Debian ปกติ ทำให้มีขนาดเล็กลงจนใกล้เคียง Alpine
ในสมัยก่อนคนส่วนใหญ่จะชอบใช้ Alpine มาก เพราะมีขนาดเล็ก และเชื่อว่าการมี binary และ lib น้อย จะช่วยให้มีปัญหาช่องโหว่น้อยลงไปด้วย แต่ Alpine เองก็สร้างปัญหาให้กับผู้พัฒนา software 3rd party เช่นกันในด้านการ maintain เพราะเป็น Linux คนละชนิด มีช่องโหว่และ bug ในแบบของตัวเอง ทำให้เกิดงานที่ต้องดูแลเพิ่มขึ้น
ภายหลังทาง Community จะแนะนำดังนี้ หากระบบไม่ได้มีปัญหาเรื่อง space มากนัก เช่น ไม่ได้ deploy บนระบบ IOT ก็ให้ใช้ tag ปกติที่มีขนาดใหญ่ หากระบบมีปัญหาเรื่อง space จำกัด ก็ให้ใช้ tag slim
ตัวอย่าง option สำหรับคำสั่ง docker run
Detached (-d)
ให้ container ทำงานใน detached mode หากไม่ใส่ option นี้ container จะทำงานใน foreground mode
Expose (-p / --publish)
Bind host port เข้ากับ container port
จากตัวอย่าง 8080:80
จะหมายถึง daemon จะสร้าง port 8080 บน host และจะ forward network packet ที่เข้ามายัง port 8080 ไปยัง port 80 ของ container
Name (--name)
กำหนดชื่อ container หากไม่กำหนดมา daemon จะ random ชื่อให้
Image[:tag]
กำหนดให้ใช้ image ตาม tag version ที่กำหนด จากตัวอย่างจะเป็น nginx version ที่มี tag เป็น 1.18
Restart Policy (--restart)
กำหนด restart policy สำหรับ container
no
เป็นค่า default กำหนดให้ไม่ต้อง restart container เมื่อ process หยุดการทำงาน
on-failure
กำหนดให้ restart เมื่อ container หยุดการทำงานและมี exit code ที่ไม่ใช่ศูนย์
always
กำหนดให้ restart ทุกครั้ง โดยไม่สนใจ exit code การกำหนดค่านี้จะทำให้ container เริ่มทำงานด้วยเมื่อ daemon startup
unless-stopped
กำหนดให้ restart container ทุกครั้ง โดยไม่สนใจ exit code รวมถึงเมื่อ daemon startup ขึ้นมาด้วย ยกเว้นแต่ว่ามีการสั่ง stop container ไป ก่อนสั่ง stop daemon
โดยปกติจะใช้ unless-stopped
Cleanup
การตรวจสอบ container
สร้าง container หนึ่งขึ้นมา สำหรับใช้ทดสอบในหัวข้อนี้ ด้วยคำสั่งดังนี้
การดู process ของแต่ละ container
การตรวจสอบการใช้งาน resource
การตรวจสอบ log ของ container
การดูรายละเอียด configuration ของ container
การรันคำสั่งภายใน container
การ shell เข้าไปภายใน container
การ shell เข้าไปภายใน container มีไว้เพื่อ diagnosis ปัญหา ไม่ได้มีไว้เพื่อให้เราเข้าไปเปลี่ยนแปลงไฟล์ภายใน container โดยตรง
หากเราต้องการเปลี่ยนแปลง content ของไฟล์บางอย่างภายใน container เราควร build image ใหม่ และ deploy container จาก image ที่ถูกแก้ไขเป็น version ใหม่นั้น
Cleanup
Docker network
โดย default หากสร้าง container โดยไม่ระบุ network แล้ว container จะ connect ไปยัง bridge
network ที่ถูกสร้างมาตั้งแต่ติดตั้ง Docker Engine
container ใดก็ตามที่อยู่ network เดียวกัน จะสามารถ access ถึงกันได้ ผ่าน private ip โดยไม่ต้องมีการ expose ด้วย option -p
เบื้องหลังการทำงานของ Docker network นั้น คือการใช้ NAT ของ service iptables แต่ผู้ใช้ไม่จำเป็นต้องรู้ว่า iptables ทำงานอย่างไร และต้องสั่ง command อะไรบ้าง ผู้ใช้เพียงแค่สั่งสร้าง Docker network ตามชนิด driver ที่มีให้ก็เพียงพอ
Network driver
Docker มี built-in driver หลายชนิดให้เลือกใช้ แต่ที่ใช้กันหลักๆ มีไม่กี่ชนิด ดังนี้
Bridge Networks
เป็น default driver หากมีการสั่งสร้าง Docker network โดยไม่มีการระบุชนิด driver
ภายในแต่ละ network จะเป็นอีก subnet หนึ่ง และ container ที่ connect เข้า network นั้นจะได้ private ip จาก network นั้น container หนึ่งๆ สามารถ connect เข้ากับหลาย network ได้
หากมีการกำหนด expose โดย option -p
จะมีการ forward traffic จาก port ของ host มายัง port ภายใน container
Host Networks
จะทำให้ network isolation ระหว่าง host และ container หายไป และให้ container ใช้งาน network ของ host ได้โดยตรง container จะมี ip ของ host นั้น และ port ที่ถูกใช้โดย container จะเท่ากับว่าถูกเปิดบน host นั้น ในทางตรงกันข้าม host จะสามารถ access ไปยัง container นั้นได้ด้วยคำว่า localhost ได้
Overlay Networks
จะเชื่อม Docker daemon จากหลายๆ เครื่องเข้าด้วยกัน ผลลัพธ์จะคล้ายกับ Bridge Networks แต่จะทำให้หลายๆ host มองเห็น network เป็นผืนเดียวกัน ทำให้ container ที่อยู่ต่าง host กัน สามารถเชื่อมต่อถึงกันได้ ผ่าน private ip ของ network นั้น ปกติใช้กับ swarm cluster
การใช้งาน Docker Network
จะเป็นการแสดง Docker network ทั้งหมดที่มีในระบบ
จากตัวอย่างจะเป็นการสร้าง network ขึ้นมาใหม่ชื่อ my_bridge
โดยใช้ driver ประเภท bridge
คำสั่ง docker network inspect ตามด้วยชื่อ network ใช้ดูรายละเอียดของ network นั้นๆ
option --network
และระบุชื่อ network จะทำให้ container มาใช้ network ตามที่ระบุ
Cleanup
การใช้งาน DNS ภายใน Docker network
จากตัวอย่างที่ผ่านๆมา จะเป็นการสร้าง container ขึ้นมา และกำหนด network ให้กับ container เพื่อ isolate ว่า container ใดบ้างที่ connect ถึงกันได้ แต่การ connect ถึงแต่ละ container โดยการใช้ private ip นั้น ขาดความสะดวก และขาดความยืดหยุ่นในการใช้งาน หาก container ถูกลบทิ้ง และสร้างขึ้นมาใหม่ ก็จะทำให้ ip address เปลี่ยนแปลงไปได้ ดังนั้นตาม best practice ควรจะอ้างอิงถึง container แต่ละตัวด้วย DNS
การใช้งาน DNS ภายใน Docker network จะทำได้ต่อเมื่อ network นั้นเป็น custom network ที่เราสร้างขึ้นมา network default ต่างๆที่มากับการติดตั้ง Docker Engine นั้น จะไม่สามารถใช้ DNS ได้
สร้าง network ชนิด bridge ขึ้นมา ชื่อ my_bridge
และสร้าง container nginx ขึ้นมาชื่อ nginx และต่อเข้า network นั้น
แล้วสร้าง container ขึ้นมาอีกตัวด้วย Image centos และเชื่อมต่อเข้า network my_bridge
จะพบว่าสามารถเรียกไปยัง container ชื่อ nginx ได้ ด้วย DNS name ว่า nginx (ตามชื่อ container)
Cleanup
Docker Image
Docker Image เป็นเสมือน template เพื่อใช้ในการสร้าง container โดยตัว Image นั้นจะเป็น read-only ไม่สามารถเขียนการเปลี่ยนแปลงลงไปใน Image ได้
การนำ Image ไปสร้างเป็น container เปรียบเสมือนการสร้างอีกชั้น layer หนึ่งขึ้นมา ซึ่งสามารถเขียนอ่านได้ การเปลี่ยนแปลงใดก็ตามที่เกิดขึ้นกับ container นั้น จะถูกเขียนลงบน layer ของ container เท่านั้น เช่น หากมีการติดตั้ง binary และ dependency เพิ่มเข้ามา ก็จะเก็บการเปลี่ยนแปลงเฉพาะส่วนต่างจาก Image ไว้
container หลายๆตัวซึ่งสร้างจาก Image เดียวกัน ก็จะมี layer read-write แยกออกจากกัน แต่ทุก container ที่สร้างจาก Image เดียวกัน ก็จะอ้างอิง layer ส่วนตั้งต้นเดียวกัน ซึ่งจะช่วยลดความซ้ำซ้อนจากส่วน base ที่เหมือนกัน ทำให้ประหยัดพื้นที่อีกด้วย
จากแนวคิดซึ่งอยู่บนพื้นฐานของการแยก layer ส่วนเปลี่ยนแปลงนั้นออกมาเป็นชั้นๆนั้น ทำให้แท้จริงแล้ว Docker Image มีเบื้องหลังเป็น กลุ่มก้อนของ layer ซึ่งแต่ละ layer เก็บข้อมูลการเปลี่ยนแปลงบน root filesystem นอกจากนี้ยังเก็บ metadata เกี่ยวกับ layer นั้นๆ และข้อมูลว่าจะรัน Image นี้อย่างไรอีกด้วย
การสร้าง Docker Image
การสร้าง Docker Image คือการนำส่วนที่มีการเปลี่ยนแปลงไป freeze เป็น read-only layer และ tag ขึ้นเป็นชื่อ Image และ/หรือ tag version ใหม่
ในทางเทคนิค สามารถทำโดยการ commit change ที่เกิดขึ้นจาก container เป็น Image โดยตรงก็ได้ แต่เป็นวิธีที่ไม่นิยม เพราะเกิด manual process และ reproduce ได้ยาก ดังนั้นปกติจะใช้วิธีการสร้างจาก Dockerfile แทน
Dockerfile จะเป็นไฟล์ซึ่งเก็บชุดคำสั่ง เพื่อใช้บอกขั้นตอนการสร้าง Docker Image ให้กับ Docker daemon และเมื่อเราสั่ง build แล้ว daemon จะไปทำงานตามคำสั่งนั้น
ตัวอย่างเช่น ทำการสร้างไฟล์ชื่อ Dockerfile
และให้มี content ภายในดังนี้
สั่งสร้าง Docker Image ด้วยคำสั่งดังนี้
daemon จะทำการอ่าน current directory(.) และหาไฟล์ชื่อ Dockerfile
ซึ่งหากพบจะทำงานตามชุดคำสั่งในไฟล์ และสร้างออกมาเป็น Docker Image
แล้วทำการ tag (option -t
) ชื่อ Image เป็นคำว่า myweb
โดยมี tag version เป็น latest
เพราะไม่ได้ระบุ tag version ไว้ขณะสั่ง build
ทดสอบนำ Image ไปรันเป็น container และเข้าหน้าเว็บเพื่อทดสอบ
Instruction ที่ใช้ภายใน Dockerfile
ภายใน Dockerfile ประกอบด้วยคำสั่งมากมาย ซึ่งใช้ในการสร้าง Docker Image แต่ละ Image ก็จะมี Instruction ต่างกันออกไป ขึ้นกับการใช้งาน
ในหัวข้อนี้ เราจะศึกษาโดยอิงจาก Dockerfile ของ Nginx tag latest เป็นตัวอย่าง
FROM
เป็นคำสั่งซึ่งระบุว่าจะสร้าง Image โดยใช้ Image ใดเป็นพื้นฐาน
ปกติจะอยู่ส่วนบนสุดของ Dockerfile นอกจากว่ามีการใช้ Instruction ARG
ร่วมด้วย ก็จะสามารถให้ Instruction ARG
ขึ้นก่อนได้
มี pattern ดังนี้
platform ใช้ระบุ architecture เช่น linux/amd64
, linux/arm64
หรือ windows/amd64
AS <name> เป็นการกำหนดชื่อให้กับ build stage นี้ ปกติจะไม่มีการระบุ นอกจากมีการทำ multi-stage build
จากตัวอย่างจะใช้ Image debian ซึ่งเป็น version buster-slim มาเป็นพื้นฐาน
LABEL
เป็นคำสั่งซึ่งกำหนดค่า metadata ของ Docker Image มี pattern ดังนี้
ในที่นี้เป็นการระบุว่า Image ถูกสร้างโดยใคร
ENV
เป็นการกำหนด environment variable ให้กับ Docker Image มี pattern ดังนี้
pattern การกำหนด ENV อีกแบบคือ
การกำหนดแบบนี้จะไม่รองรับการประกาศหลาย variable และไม่แนะนำให้ใช้ เพราะเป็น syntax แบบเก่า และอาจถูกยกเลิกการรองรับในอนาคต
จากตัวอย่างจะมีการกำหนด version Nginx ซึ่งจะถูกเรียกใช้ในคำสั่งอื่นๆ ต่อไป
RUN
เป็นการกำหนดคำสั่งซึ่งจะถูกนำไป execute ภายใต้ shell ซึ่ง default คือ /bin/sh มี pattern ดังนี้
หรืออีก pattern ดังนี้
จากตัวอย่างจะเป็นคำสั่งติดตั้ง software Nginx
ข้อควรระวัง
ในการ build image ของ Docker นั้น แต่ละ instruction จะแทน layer หนึ่งๆ ดังนั้นจึงนิยมใช้ shell command หลายๆ คำสั่งต่อๆ กัน ภายใต้ RUN layer เดียว การแยก command ออกไปคนละ layer อาจจะทำให้ Image ใหญ่ขึ้นโดยไม่จำเป็น
เช่น หากเราแยก RUN layer แรกเป็นการ download zip file มา, RUN layer ถัดไปสั่ง extract file, RUN layer ถัดไป สั่งลบไฟล์ zip ทิ้ง ก็อาจจะได้ขนาด Image เกือบสองเท่าของที่ควรจะเป็น เพราะ layer หนึ่งเป็น size zip file, layer ถัดมาขนาดเท่าไฟล์ทั้งหมดที่ถูก extract และ layer สุดท้ายขนาดเท่าการ mark delete zip file
แต่หากเราทำทั้ง 3 คำสั่งภายใต้ layer เดียวแล้ว Docker จะ copy มาเฉพาะส่วน data ที่คงอยู่ ซึ่งจะขนาดตามไฟล์ที่ถูก extract เท่านั้น ไม่รวมขนาดไฟล์ zip มาด้วย
นั่นจึงเป็นที่มาว่าทำไม RUN layer มักจะประกอบด้วย && ระหว่างแต่ละ shell command
COPY
ใช้คัดลอกไฟล์จาก Host เข้าไปยัง directory ของ Container Image มี pattern ดังนี้
หรือ
โดยไฟล์ที่ถูกคัดลอกลงไปจะมี UID และ GID เป็น 0 (root) หากไม่มีการใส่ option --chown
ข้อควรระวัง
หากต้องการไฟล์เป็น owner และ/หรือ group อื่น แล้วไม่ได้เปลี่ยนตั้งแต่ขั้นตอนนี้ แต่ไปใช้ RUN layer เพื่อ chown อีกต่อหนึ่ง จะทำให้ขนาด Image ใหญ่ขึ้นได้โดยไม่จำเป็น
src
สามารถเป็น wildcard ได้ เช่น myfile/*
dest
สามารถเป็น relative path ได้ โดยจะถูกไปอ้างอิงกับ path ที่กำหนดโดย WORKDIR
ENTRYPOINT
เป็นการระบุให้ Docker Engine รู้ว่า เมื่อ container ถูกสร้างขึ้นมา จะ execute คำสั่งใด มี pattern ดังนี้
จากตัวอย่างจะเป็นการบอกว่า เมื่อ container Nginx ถูกสร้างขึ้นมา ให้ execute script /docker-entrypoint.sh
ซึ่ง script นี้ หุ้มคำสั่งซึ่งจะ start process Nginx อีกทีหนึ่ง
EXPOSE
เป็นการบอกให้ทราบว่า Container Image นี้ จะ listen network port อะไรบ้าง คำสั่งนี้ไม่ได้เป็นการ publish port ให้กับผู้ใช้ แต่เป็นแค่ information ในการสื่อสารให้ผู้ที่นำ Container Image นี้ไป deploy ทราบว่า Image จะใช้ port อะไรบ้าง และให้ผู้ deploy publish เองว่าจะ map port ใดของ Host มายัง port ภายใน container มี pattern ดังนี้
จากตัวอย่างจะบอกให้ผู้ใช้ทราบว่า Nginx Image นี้ listen port 80
STOPSIGNAL
เป็นการกำหนด systemcall signal ที่จะเกิดขึ้นเมื่อสั่งหยุดการทำงานของ container
หากไม่กำหนด โดย default จะส่ง SIGTERM
มายัง process
มี pattern ดังนี้
จากตัวอย่างจะกำหนดให้ส่ง SIGQUIT
มายัง process Nginx
CMD
เป็นคำสั่งเพื่อบอกให้ Docker Engine รู้ว่าจะเริ่มทำงานอย่างไรเมื่อ container ถูกสร้าง
จะมีพฤติกรรมต่างกันออกไป ขึ้นอยู่กับว่ามีการกำหนด ENTRYPOINT
ไว้หรือไม่
หากไม่มีการกำหนด ENTRYPOINT
ปกติจะใช้ในรูป
หากมีการกำหนด ENTRYPOINT
ไว้ จะทำงานเปรียบเสมือนเป็นแค่ argument ให้กับ ENTRYPOINT
และจะมีรูปแบบดังนี้
ซึ่งจากตัวอย่างของ Nginx นั้น จะมี ENTRYPOINT
กำหนดไว้ จึงทำหน้าที่แค่ pass argument "nginx", "-g", "daemon off;" ให้กับ /docker-entrypoint.sh
(ENTRYPOINT
script)
Docker Storage
การใช้งาน Container จะมีลักษณะที่เป็น Immutable และ Ephemeral การเปลี่ยนแปลงใดๆก็ตามบน Container จะไม่ส่งผลไปยัง Container Image ดังนั้น เมื่อ Container ถูกลบทิ้งและสร้างใหม่จาก Image เดิม ก็จะกลับไปยังสถานะตาม Image ต้นฉบับ
เมื่อเป็นเช่นนี้ การจะเก็บรักษาข้อมูลบางส่วนที่ถูกเปลี่ยนแปลงไว้ เช่น ข้อมูลที่เปลี่ยนแปลงใน Database ก็จะต้องถูกเก็บอยู่ในส่วนอื่นนอก Container ใน Docker เอง ก็มีวิธีการนำพื้นที่ส่วนอื่นมา mount ทับบน Container และเขียนอ่านบนพื้นที่นั้นได้ ซึ่งทำได้หลายวิธี เช่น ใช้ Volume mount หรือใช้ Bind mount
Volumes
Volume เป็น resource ชนิดหนึ่งที่ถูกจัดการโดย Docker เมื่อเราสั่งสร้าง Volume ขึ้นมานั้น Docker จะไป allocate พื้นที่ส่วนหนึ่งมาทำเป็น Volume ซึ่งพื้นที่นั้นอาจจะอยู่บนเครื่อง Server อื่นก็ได้ หากเรามีการกำหนดไว้เช่นนั้น
Volume จะถูก mount เป็นส่วนหนึ่งของ filesystem บน Container เมื่อ Container ถูกสร้างขึ้นมา เมื่อมีการลบ Container ไป Volume จะยังคงอยู่ตามเดิม ดังนั้น เราสามารถสร้าง Container ใหม่ขึ้นมาโดยผูกกับ Volume เดิมได้
โดย Default นั้น Volume จะถูกสร้างขึ้นโดยมี random unique ID แต่จะสร้างความยุ่งยากในการอ้างอิงตอนสั่ง map Container เข้ากับ Volume เดิม ดังนั้น เราสามารถสร้าง Volume โดยกำหนดชื่อไว้ก่อนได้ ซึ่งวิธีนี้ เรียกว่าการสร้าง named Volume และเราจะใช้ name นั้นในการ map Container เข้ากับ Volume
การสร้าง Volume
การ list Volume
การลบ Volume
การสร้าง Container โดย map กับ Volume
การ map Volume ให้กับ Container ใช้ options -v เมื่อสั่ง docker run โดยมี pattern ดังนี้
ทดสอบการ persistent data
cleanup
Bind mounts
เป็นวิธีการ mount path บน Server เข้าไปยัง Container คำสั่งในการใช้งาน Bind mount จะคล้ายกับการใช้งาน Volume แต่ต่างกันตรง แทนที่จะอ้างอิงเป็นชื่อ named volume จะอ้างอิงเป็น path บน Server แทน
ทดสอบการใช้ Bind mount
จะพบว่าภายใต้ directory นั้น จะมีไฟล์อยู่ เพราะเป็นไฟล์ที่ถูกเขียนลงมาโดย container เอง
cleanup
Docker Compose
เนื่องจากการใช้งาน docker command มีความยุ่งยาก มี options ที่ต้องใส่มากมาย ซึ่งจะทำให้เกิดความผิดพลาดได้หากมีการใส่ไม่ครบตามเดิม ดังนั้นการที่มี file หนึ่งซึ่งเก็บรายละเอียด config ทั้งหมดไว้ ก็จะลดความผิดพลาดในส่วนนั้นได้ และง่ายต่อการแชร์ให้คนอื่น นอกจากนี้การที่ config ทุกอย่างถูกเก็บเป็นไฟล์นั้น เราก็สามารถนำ file นั้นเก็บเป็น version ต่างๆและถูกควบคุมด้วยเครื่องมือประเภท source control ได้อีกด้วย
ใน Docker นั้น เครื่องมือที่ใช้ในการอ่าน config file และติดต่อกับ Docker Engine ก็คือ Docker Compose
Docker Compose จะอ่านไฟล์ชื่อ docker-compose.yml หรือ docker-compose.yaml โดย default ภายในไฟล์จะเป็น configuration ในรูปแบบ YAML format
ตัวอย่าง compose file
version
เป็นส่วนระบุ syntax version ของ Compose file ถ้า version ใหม่ไป Docker Compose อาจจะไม่รู้จัก และไม่สามารถแปลได้ แต่ถ้าเก่าไป ก็อาจจะขาด feature แล้วไม่ compat กับ Docker Engine
services
เป็นส่วนที่จะระบุ container ที่จะถูกสร้างใน stack app นี้ ซึ่งชื่อภายใต้ service นี้จะเป็นอะไรก็ได้ อาจจะไม่ต้องตรงกับชื่อ Container Image
volumes
เป็นส่วนที่จะระบุ named volume ที่จะถูกสร้างใน stack app นี้
-------------------------------------------------------------------------------
Configuration ภายใต้แต่ละ services
restart
เป็นส่วนกำหนด restart policy ของแต่ละ container
depends_on
ใช้เพื่อระบุว่า ต้องรออีก service เริ่มทำงานก่อน
image
ระบุ Container Image และ Tag version ซึ่งนำมาใช้สร้าง Container
ports
ระบุ port ที่จะ map bridge จาก Host เข้าไปยัง Container
volumes
ระบุ named Volume หรือ Bind mount ที่จะ mount เข้าไปยัง container
ในกรณีใช้ Bind mount จะใช้ $(pwd) ไม่ได้ ให้ refer directory จากชั้นเดียวกับ docker compose file ด้วย ./ เช่น ./datadir:/var/lib/mysql
environment
กำหนดชื่อ environment และค่า ซึ่งจะถูกใช้ภายใน container
การ Deploy stack ด้วย Docker Compose
สร้าง container ทั้ง stack และทำงานใน detach mode ด้วยคำสั่งดังนี้
ในกรณีที่ไม่ต้องการลบ stack ทิ้ง แต่ต้องการ stop เท่านั้น ใช้คำสั่งดังนี้
ในกรณีต้องการ start กลับมา ใช้คำสั่งดังนี้
กรณีที่ต้องการดำเนินการกับเพียงบาง Container เท่านั้น สามารถทำได้โดยระบุชื่อ service (ใน Compose file) ต่อท้าย เช่น
สั่งลบทั้ง stack ด้วยคำสั่งดังนี้
Last updated
Was this helpful?