บทสุดท้ายแล้ว! เราจะมารู้จัก “container” — กล่องที่ห่อแอปพร้อมทุกอย่างที่มันต้องใช้ แล้วเอาไปรันที่ไหนก็ได้ และจะปิดท้ายด้วยลูกเล่นเด็ด: ทำให้ container เปิดเองอัตโนมัติทุกครั้งที่บูตเครื่อง
🌱 อ่านต่อจาก M1–M13 ได้สบาย ไม่ต้องมีพื้นฐาน container มาก่อนลองนึกถึงปัญหาคลาสสิกของโปรแกรมเมอร์: เขียนแอปบนเครื่องตัวเองรันได้สวยงาม พอเอาไปลงเครื่องเพื่อนหรือเซิร์ฟเวอร์จริง — พังหมด! เพราะเวอร์ชัน library ไม่ตรง ขาดไฟล์โน่นนี่ ตั้งค่าคนละแบบ “แต่บนเครื่องผมรันได้นะ” คือประโยคในตำนาน
Container เกิดมาเพื่อแก้ปัญหานี้ — มันคือ กล่องที่ห่อแอปพร้อมทุกอย่างที่แอปต้องใช้ (โค้ด, library, ไฟล์ตั้งค่า, runtime) ไว้ด้วยกัน พอเอากล่องนี้ไปเปิดที่เครื่องไหนก็ตาม มันจะรันได้เหมือนกันเป๊ะ
นึกถึง กล่องข้าวที่ห่อกับข้าวมาครบชุด — มีข้าว มีกับ มีช้อนส้อม มีน้ำจิ้ม ครบในกล่องเดียว เปิดที่ไหนก็กินได้เลย ไม่ต้องวิ่งหาเครื่องปรุงใหม่ ไม่ต้องกลัวว่า “บ้านนี้ไม่มีน้ำปลา”
หรืออีกภาพหนึ่ง: ตู้คอนเทนเนอร์ขนส่งสินค้า ที่มีขนาดมาตรฐานเดียวกันทั้งโลก เรือลำไหน รถบรรทุกคันไหน เครนตัวไหน ก็ยกได้หมด ข้างในจะเป็นรองเท้าหรือกล้วยก็ไม่สำคัญ — “กล่องข้างนอกมาตรฐานเดียวกัน” คือหัวใจ (คำว่า container ก็มาจากตู้ขนส่งนี่แหละ)
หลายคนเคยได้ยิน VM — เครื่องเสมือนที่จำลองคอมพิวเตอร์ทั้งเครื่องขึ้นมา ทั้งสองอย่างนี้ช่วย “แยกแอปออกจากกัน” ได้เหมือนกัน แต่หนักเบาต่างกันมาก
| VM | Container | |
|---|---|---|
| จำลองอะไร | ทั้งเครื่อง + ระบบปฏิบัติการเต็มตัว | แค่แอป + ของที่แอปต้องใช้ |
| ขนาด | หนักเป็น GB | เบา มักหลัก MB |
| เปิดเร็วแค่ไหน | เป็นนาที (ต้องบูต OS) | เป็นวินาที |
| ใช้ kernel | ของตัวเอง | ใช้ของเครื่อง host ร่วมกัน |
VM = สร้างบ้านใหม่ทั้งหลัง (มีฐานราก หลังคา ระบบไฟ ครบทุกอย่าง) ส่วน Container = เช่าห้องในคอนโด ที่ใช้โครงสร้างตึก (kernel) ร่วมกัน — แค่กั้นห้องส่วนตัวให้แต่ละแอปอยู่ จึงเบากว่าและเปิดเร็วกว่ามาก
ในโลก container เครื่องมือที่ดังที่สุดคือ Docker แต่ บน RHEL 9 (และ Red Hat) เราใช้ Podman ไม่ใช่ Docker — ข้อสอบ RHCSA ก็ถาม Podman ดังนั้นบทนี้เราโฟกัสที่ Podman
ข่าวดีคือคำสั่ง Podman เกือบทั้งหมด พิมพ์เหมือน Docker เป๊ะ แค่เปลี่ยนคำว่า docker เป็น podman ใครเคยใช้ Docker มาก็ใช้ Podman ได้ทันที
Podman รันได้โดย user ธรรมดา (ไม่ต้องเป็น root!) — เราเรียกว่า rootless container
ทำไมถึงสำคัญ? เพราะถ้า container โดนเจาะ คนร้ายก็ได้แค่สิทธิ์ของ user ธรรมดาคนนั้น ไม่ได้สิทธิ์ root ของทั้งเครื่อง — ปลอดภัยกว่าแบบที่ต้องรันเป็น root มาก นี่คือเหตุผลใหญ่ที่ Red Hat เลือก Podman
เหมือนให้พนักงานคนหนึ่ง กุญแจห้องตัวเองดอกเดียว แทนที่จะให้ กุญแจมาสเตอร์ที่เปิดได้ทุกห้อง — ถ้ากุญแจหลุดมือไป ความเสียหายก็จำกัดอยู่แค่ห้องเดียว ไม่ลามทั้งตึก
Docker ต้องมีโปรเซสตัวกลางชื่อ daemon รันค้างเป็น root ตลอดเวลา แต่ Podman ไม่มี daemon — สั่ง podman run ทีไรมันก็รัน container ขึ้นมาตรงๆ จึงเรียบง่ายและปลอดภัยกว่า
ก่อนจะรัน container ได้ เราต้องมี image ก่อน — image คือ “แม่พิมพ์” หรือ “พิมพ์เขียว” ของ container ส่วน container คือตัวที่ถูกปั๊มออกมาจากแม่พิมพ์นั้นแล้วกำลังทำงานอยู่
image = สูตรขนม + วัตถุดิบที่บรรจุไว้ (อ่านอย่างเดียว เปลี่ยนไม่ได้) ส่วน container = ขนมที่อบเสร็จออกมาจากสูตรนั้น — จาก image เดียวจะปั๊ม container ออกมากี่ตัวก็ได้ เหมือนสูตรเดียวอบขนมได้หลายถาด
[student@server1 ~]$ podman search nginx NAME DESCRIPTION registry.access.redhat.com/... Nginx web server ... docker.io/library/nginx Official build of Nginx
ค้นหา image ชื่อ nginx จาก registry ที่เครื่องเรารู้จัก คอลัมน์ NAME คือชื่อเต็มที่เอาไปใช้ pull ต่อได้
[student@server1 ~]$ podman pull registry.access.redhat.com/ubi9/httpd-24 Trying to pull registry.access.redhat.com/ubi9/httpd-24:latest... Getting image source signatures Writing manifest to image destination Storing signatures
รูปแบบชื่อ image เต็มๆ คือ registry/ชื่อ:tag
registry...ที่อยู่ของคลัง image (เช่น registry.access.redhat.com)ubi9/httpd-24ชื่อ image (UBI = Universal Base Image ของ Red Hat):tagเวอร์ชัน เช่น :latest ถ้าไม่ใส่จะเดาเป็น :latest ให้[student@server1 ~]$ podman images REPOSITORY TAG IMAGE ID SIZE registry.access.redhat.com/ubi9/httpd-24 latest 3f2a1b... 461 MB
แสดงรายการ image ที่ดึงมาเก็บไว้แล้ว (เหมือน ls แต่สำหรับ image)
เวลาเราพิมพ์ podman search nginx เฉยๆ ไม่ได้ระบุที่อยู่เต็ม Podman จะรู้ได้ยังไงว่าควรไปค้นที่คลังไหน? คำตอบอยู่ในไฟล์ตั้งค่า
registry.access.redhat.com, docker.ioไฟล์ที่กำหนดว่า registry ไหนเชื่อถือได้และค้นเรียงตามลำดับไหน คือ:
# รายชื่อ registry ที่จะค้นหา เรียงตามลำดับ unqualified-search-registries = ["registry.access.redhat.com", "docker.io"]
บรรทัด unqualified-search-registries บอกว่า ถ้าเราพิมพ์ชื่อ image แบบไม่เต็ม (เช่น แค่ nginx) ให้ไปค้นที่ registry ในลิสต์นี้ เรียงทีละตัว
ไฟล์ตั้งค่าระดับเครื่องอยู่ที่ /etc/containers/registries.conf ส่วนของ user แต่ละคนอยู่ที่ ~/.config/containers/registries.conf — ในห้องสอบมักไม่ต้องแก้ไฟล์นี้ลึก แต่ควร รู้ว่ามันอยู่ไหนและมีไว้ทำอะไร
podman runคำสั่งหัวใจของบทนี้ มี option เยอะแต่ค่อยๆ ทำความเข้าใจทีละตัว มาดูตัวอย่างจริงที่ใช้ option สำคัญครบ
[student@server1 ~]$ podman run -d --name web1 \ -p 8080:80 \ -v ~/webdata:/var/www/html:Z \ -e MY_VAR=hello \ registry.access.redhat.com/ubi9/httpd-24 a1b2c3d4e5f6... ← container ID ที่ได้คืนมา
แปล option ทีละตัว — เครื่องหมาย \ ท้ายบรรทัดแปลว่า “ยังไม่จบ บรรทัดถัดไปคือคำสั่งเดียวกัน” (ช่วยให้อ่านง่าย):
-ddetached = รันค้างอยู่ เบื้องหลัง ไม่ยึดหน้าจอเรา (ถ้าไม่ใส่ มันจะค้างที่หน้าจอจนกว่าจะกด Ctrl+C)--name web1ตั้งชื่อ container ว่า web1 เพื่อให้เรียกใช้ง่าย ถ้าไม่ตั้ง Podman จะสุ่มชื่อแปลกๆ ให้-p 8080:80map port host:container = ใครเข้า port 8080 ของเครื่อง host จะถูกส่งต่อไป port 80 ในตัว container-v ~/webdata:/var/www/html:Zmap โฟลเดอร์ host:container — ดูข้อ 7 และอย่าลืม :Z สำหรับ SELinux!-e MY_VAR=helloส่ง environment variable เข้าไปใน container (ดูข้อ 8)นึกถึง ตู้ ปณ. หน้าบ้าน — container ซ่อนอยู่ในบ้าน ไม่มีใครเข้าถึงได้ตรงๆ -p 8080:80 ก็เหมือนติดป้ายว่า “จดหมายที่ส่งมาที่ตู้เลข 8080 หน้าบ้าน ให้เอาไปส่งห้องเลข 80 ข้างใน” — โลกภายนอกเคาะที่ 8080 ของ host ของจริงทำงานที่ 80 ข้างใน container
พอมี container แล้ว เราต้องดูแลมันเป็น — ดูว่ามีตัวไหนรันอยู่ หยุด เปิดใหม่ ลบ ดู log และมุดเข้าไปข้างใน
[student@server1 ~]$ podman ps CONTAINER ID IMAGE STATUS NAMES a1b2c3d4e5f6 .../httpd-24 Up 2 minutes web1 [student@server1 ~]$ podman ps -a (แสดงทั้งหมด รวมตัวที่หยุดไปแล้วด้วย) [student@server1 ~]$ podman stop web1 [student@server1 ~]$ podman start web1 [student@server1 ~]$ podman logs web1 [student@server1 ~]$ podman rm web1
podman psดู container ที่ กำลังรันอยู่ (เทียบได้กับ ls ของ container)podman ps -aดู ทั้งหมด รวมตัวที่หยุดไปแล้ว (-a = all)podman stop web1หยุด container (มันยังอยู่ แค่ไม่ทำงาน)podman start web1เปิด container ที่หยุดไว้ให้กลับมาทำงานpodman logs web1ดู log / ข้อความที่ container พ่นออกมา — ใช้เวลามันพัง อยากรู้ว่าเกิดอะไรขึ้นpodman rm web1ลบ container ทิ้ง (ต้อง stop ก่อน หรือใช้ rm -f บังคับ)บางทีเราอยากเข้าไป “ข้างในกล่อง” เพื่อดูไฟล์หรือแก้อะไรสักอย่าง ใช้ exec -it ... bash
[student@server1 ~]$ podman exec -it web1 bash bash-5.1$ ls /var/www/html index.html bash-5.1$ exit ← พิมพ์ exit เพื่อออกกลับมาที่เครื่อง host
execสั่งให้รันคำสั่งหนึ่ง ภายใน container ที่กำลังรันอยู่-it-i interactive + -t terminal = ขอ shell แบบโต้ตอบได้ (พิมพ์–เห็นผลทันที)bashคำสั่งที่อยากรันข้างใน — ในที่นี้คือเปิด shell bash เพื่อพิมพ์คำสั่งต่อนี่คือเรื่องสำคัญที่มือใหม่มักสะดุด: container เป็นของชั่วคราว — พอลบ container ทิ้ง ข้อมูลที่อยู่ ข้างใน มันก็หายไปด้วยทั้งหมด!
container เหมือน ห้องพักโรงแรม — เช็คเอาท์ (ลบ container) เมื่อไหร่ ของที่วางไว้ในห้องก็ถูกเก็บกวาดหมด ถ้าอยากเก็บของไว้ ต้อง ฝากไว้ในเซฟของโรงแรมที่อยู่นอกห้อง (= โฟลเดอร์บนเครื่อง host) ของในเซฟจะอยู่ต่อแม้คุณเช็คเอาท์ไปแล้ว
วิธีฝากข้อมูลไว้นอก container คือ bind mount ด้วย option -v — เอาโฟลเดอร์จากเครื่อง host ไป “ผูก” กับโฟลเดอร์ใน container เมื่อ container เขียนข้อมูล มันจะไปลงที่โฟลเดอร์ host จริงๆ ลบ container ทิ้งข้อมูลก็ยังอยู่
[student@server1 ~]$ mkdir ~/webdata [student@server1 ~]$ podman run -d --name web1 \ -p 8080:80 \ -v ~/webdata:/var/www/html:Z \ registry.access.redhat.com/ubi9/httpd-24
อ่านค่า -v เป็น 3 ส่วน คั่นด้วย : → โฟลเดอร์host : โฟลเดอร์ในcontainer : ตัวเลือก
~/webdataโฟลเดอร์จริงบนเครื่อง host (ข้อมูลตัวจริงเก็บที่นี่)/var/www/htmlจุดที่ container จะเห็นโฟลเดอร์นั้น:Zสำคัญมาก! สั่งให้ Podman ปรับ SELinux label ให้ container เข้าถึงโฟลเดอร์นี้ได้จำได้ไหมว่า RHEL เปิด SELinux ไว้เป็นยาม ถ้าเราเอาโฟลเดอร์ host ใส่เข้า container โดยไม่เติม :Z SELinux จะมอง label ไม่ตรงแล้ว บล็อกไม่ให้ container อ่าน/เขียน ผลคือเว็บขึ้น “Permission denied” ทั้งที่สิทธิ์ไฟล์ดูปกติดี
เติม :Z ต่อท้าย แล้ว Podman จะ relabel โฟลเดอร์นั้นให้อัตโนมัติ — นี่คือกับดักยอดฮิตในห้องสอบ
:Z (ใหญ่) = label แบบ เฉพาะ container นี้ตัวเดียว ใช้ตัวนี้เป็นค่ามาตรฐาน ส่วน :z (เล็ก) = แชร์ให้หลาย container ใช้ร่วมกันได้ ตอนเริ่มต้นจำแค่ :Z ใหญ่ก็พอ
หลาย image ต้องการค่าตั้งต้นตอนเปิด เช่น image ฐานข้อมูลอาจอยากได้ชื่อ database หรือรหัสผ่าน admin เราป้อนค่าพวกนี้ผ่าน option -e (environment)
[student@server1 ~]$ podman run -d --name db1 \ -e POSTGRESQL_USER=appuser \ -e POSTGRESQL_PASSWORD=secret123 \ -e POSTGRESQL_DATABASE=appdb \ registry.redhat.io/rhel9/postgresql-15
แต่ละ -e ชื่อ=ค่า คือตัวแปร 1 ตัวที่ถูกส่งเข้าไปใน container อยากใส่กี่ตัวก็เติม -e ซ้ำได้ — แอปข้างใน container จะอ่านค่าพวกนี้ไปตั้งค่าตัวเองตอนเริ่มทำงาน
เหมือน โพสต์อิทแปะหน้ากล่อง ก่อนส่งกล่องเข้าไปทำงาน เช่น “ผู้ใช้ชื่อ appuser, รหัส secret123” — พอ container เปิดขึ้นมา มันอ่านโพสต์อิทพวกนี้แล้วเซ็ตตัวเองตามนั้น โดยที่เราไม่ต้องเข้าไปแก้ไฟล์ข้างในเลย
มุดเข้าไปด้วย podman exec -it db1 env จะเห็นรายการ environment variable ทั้งหมดที่ container ตัวนั้นมองเห็น
นี่คือ หัวใจของบทนี้และออกสอบแน่นอน — ปัญหาคือ ถ้าเซิร์ฟเวอร์รีบูต container ที่เราเปิดไว้จะ ไม่เด้งกลับมาเอง เราต้องสั่งให้ระบบเปิดมันให้อัตโนมัติ วิธีของ RHEL 9 คือผูก container เข้ากับ systemd (ตัวจัดการ service ที่เราเรียนมาแล้วใน M9)
เนื่องจาก Podman เรารันแบบ rootless (user ธรรมดา) เราจึงใช้ systemd ระดับ user คือ systemctl --user ทำตาม 4 ขั้นนี้
[student@server1 ~]$ mkdir -p ~/.config/systemd/user [student@server1 ~]$ cd ~/.config/systemd/user [student@server1 user]$ podman generate systemd --name web1 --files --new /home/student/.config/systemd/user/container-web1.service
generate systemdให้ Podman เขียนไฟล์ systemd service ให้เราอัตโนมัติ ไม่ต้องเขียนเอง--name web1อ้างถึง container ชื่อ web1 ที่เราสร้างไว้--filesให้เขียนออกมาเป็น ไฟล์ (ในโฟลเดอร์ปัจจุบัน) แทนที่จะพ่นบนจอเฉยๆ--newให้ service สร้าง container ใหม่ ทุกครั้งที่เริ่ม (แนะนำ — ทำให้ย้ายเครื่อง/ทำซ้ำได้ง่าย)service ระดับ user เก็บที่ ~/.config/systemd/user/ — ถ้า generate จากที่อื่น ให้ คัดลอกไฟล์ .service มาไว้ในโฟลเดอร์นี้ systemd ถึงจะมองเห็น
[student@server1 ~]$ systemctl --user daemon-reload [student@server1 ~]$ systemctl --user enable --now container-web1.service Created symlink .../container-web1.service ...
--userสั่งกับ systemd ระดับ user ของเรา (ไม่ใช่ระดับทั้งระบบ — ห้ามลืม!)daemon-reloadบอก systemd ว่า “มีไฟล์ service ใหม่นะ ไปอ่านซะ”enable --nowenable = เปิดเองทุกครั้งที่บูต + --now = เริ่มรันเดี๋ยวนี้เลยด้วย[student@server1 ~]$ loginctl enable-linger student [student@server1 ~]$ loginctl show-user student | grep Linger Linger=yes
โดยปกติ service ระดับ user จะรัน ก็ต่อเมื่อ user คนนั้นล็อกอินอยู่ เท่านั้น พอ logout ออก service (และ container) ก็ดับตามไปด้วย — แปลว่าหลังรีบูตที่ยังไม่มีใคร login เข้ามา container จะ ไม่ขึ้น!
loginctl enable-linger student สั่งให้ระบบ ยอมให้ service ของ user คนนี้รันต่อแม้ไม่ได้ login — นี่แหละที่ทำให้ container เด้งขึ้นเองหลังบูต
ทำครบทุกอย่างแต่ ลืม loginctl enable-linger คือกับดักอันดับหนึ่งของข้อนี้! เพราะตอนทดสอบมือ (ขณะ login อยู่) มันทำงานปกติ ดูเหมือนถูกหมด — แต่พอ รีบูต เครื่องตรวจคำตอบ container กลับไม่ขึ้น เพราะไม่มีใคร login → คะแนนหาย
กฎจำง่าย: “rootless container เปิดตอนบูต ต้องคู่กับ enable-linger เสมอ”
[student@server1 ~]$ sudo reboot (รอเครื่องบูตเสร็จ แล้ว login ใหม่) [student@server1 ~]$ podman ps CONTAINER ID IMAGE STATUS NAMES a1b2c3d4e5f6 .../httpd-24 Up 30 seconds web1 ← ขึ้นเองแล้ว!
ถ้า podman ps หลังรีบูตเห็น container ขึ้นเอง = สำเร็จ! ในห้องสอบ ควรรีบูตทดสอบจริง อย่าเชื่อแค่ว่า “ตอนนี้รันอยู่” เพราะข้อสอบจะตรวจหลังรีบูต
loginctl enable-linger: container เปิดเองได้ตอน login อยู่ แต่ หายหลังรีบูต เพราะ user service ไม่รันเมื่อไม่มีใคร login — กับดักอันดับหนึ่งของบทนี้:Z ตอน mount volume: SELinux บล็อกไม่ให้ container เข้าถึงโฟลเดอร์ เจอ “Permission denied” ทั้งที่สิทธิ์ไฟล์ดูปกติ — เติม :Z ท้าย -v เสมอsystemctl --user กับระดับระบบ: rootless container ต้องใช้ --user ทุกคำสั่ง ถ้าพิมพ์ systemctl enable เฉยๆ (ระดับ system) จะหา service ไม่เจอ--now ตอน enable: enable เฉยๆ แค่ตั้งให้เปิด “ครั้งหน้า” แต่ตอนนี้ยังไม่รัน — เติม --now เพื่อให้เริ่มทันทีด้วย (หรือ start แยกอีกที)daemon-reload: วางไฟล์ .service ใหม่แล้วแต่ไม่ reload systemd ยังมองไม่เห็น สั่ง enable แล้ว error.service ผิดที่: service ระดับ user ต้องอยู่ใน ~/.config/systemd/user/ เท่านั้น ไม่ใช่ /etc/systemd/system/| คำสั่ง | ทำอะไร |
|---|---|
podman search ชื่อ | ค้นหา image จาก registry |
podman pull registry/image:tag | ดึง image ลงมาเก็บในเครื่อง |
podman images | ดู image ที่มีในเครื่อง |
podman run -d --name x image | รัน container เบื้องหลัง พร้อมตั้งชื่อ |
-p 8080:80 | map port host:container |
-v /host:/ctr:Z | map โฟลเดอร์ (อย่าลืม :Z เรื่อง SELinux) |
-e VAR=ค่า | ส่ง environment variable เข้า container |
podman ps / ps -a | ดู container ที่รันอยู่ / ทั้งหมด |
podman stop x / start x | หยุด / เปิด container |
podman rm x | ลบ container ทิ้ง |
podman logs x | ดู log ของ container |
podman exec -it x bash | มุดเข้าไปใน container เปิด shell |
podman generate systemd --name x --files --new | สร้างไฟล์ .service ของ container |
systemctl --user daemon-reload | ให้ systemd อ่านไฟล์ service ใหม่ |
systemctl --user enable --now container-x.service | เปิดอัตโนมัติตอนบูต + เริ่มเดี๋ยวนี้ |
loginctl enable-linger user | ⭐ ให้ user service รันแม้ไม่ได้ login |
ดึง image ลงมา (pull) → รัน container พร้อม map port/volume/env (run) → ดูแลมัน (ps/stop/logs/exec) → เก็บข้อมูลไม่ให้หายด้วย volume + :Z → และทำให้ container เปิดเองหลังบูต ด้วย systemd user service + enable-linger ครบสูตรออกสอบ!
จาก M1 ที่ยัง “กลัวหน้าจอดำๆ” มาจนถึง M14 ที่ทำ container เปิดเองตอนบูตได้ — คุณเดินทางมาไกลมาก! ตอนนี้คุณมีพื้นฐาน RHCSA ครบทั้งหลักสูตรแล้ว
ก้าวต่อไป: ลงมือทำ lab จริงซ้ำๆ ให้มือจำคำสั่งได้เอง แล้วลอง mock exam จับเวลา 3 ชั่วโมงเหมือนสอบจริง ยิ่งฝึกบนเครื่องจริงมากเท่าไหร่ วันสอบยิ่งสบายเท่านั้น โชคดีนะ! 🚀