เปรียบเทียบ Podman กับ Docker บน VPS เลือกตัวไหนดี
เจาะลึกความต่างระหว่าง Podman และ Docker บน VPS เมื่อไม่มี daemon และเน้น rootless จะส่งผลต่อการจัดการไฟล์ compose, quadlets, พอร์ต และสิทธิ์ของ volume อย่างไรในทางปฏิบัติ
ความแตกต่างที่แท้จริงระหว่าง Podman และ Docker
Podman และ Docker รัน OCI (Open Container Initiative) image ชุดเดียวกันบน VPS ดังนั้นการเลือกใช้งานจึงไม่ใช่เรื่องของซอฟต์แวร์ที่สามารถรันได้ แต่ความแตกต่างอยู่ที่รูปแบบกระบวนการทำงาน (process model) Docker รัน root daemon ซึ่งเป็นเจ้าของ container ทุกตัว และคำสั่ง docker เป็นเพียงไคลเอนต์ขนาดเล็กที่ส่งคำขอให้ daemon ทำงานนั้นๆ ส่วน Podman ไม่มี daemon โดย podman run จะเริ่ม container ในฐานะกระบวนการลูก (child process) ของสิ่งที่เรียกใช้มัน ภายใต้สิทธิ์ของผู้ใช้ทั่วไป (unprivileged user) ของคุณเอง
ทุกอย่างที่เหลือเป็นผลสืบเนื่องมาจากข้อเท็จจริงข้างต้น การเริ่มทำงานอัตโนมัติ (auto-start) จึงกลายเป็นหน้าที่ของ systemd แทนที่จะเป็นหน้าที่ของ daemon การเป็นเจ้าของ volume จะผ่าน user namespace ดังนั้นเจ้าของที่คุณเห็นด้วยคำสั่ง ls -l บนโฮสต์จะไม่ใช่เจ้าของเดียวกับที่ container มองเห็น พอร์ตที่ต่ำกว่า 1024 จะไม่สามารถ bind ได้จนกว่าคุณจะเปลี่ยนการตั้งค่า kernel ส่วน CLI (command line interface) ของ docker ยังคงทำงานได้ผ่าน wrapper จนกระทั่งถึงจุดที่บริการบางอย่างต้องการใช้งาน Docker socket
ไม่มี daemon: สิ่งที่ทำงานจริงเมื่อคุณเริ่ม container
บน Docker host คำสั่ง pstree -a จะแสดง dockerd ในฐานะ root โดยมี containerd อยู่ข้างๆ และมี containerd-shim-runc-v2 หนึ่งรายการต่อ container ที่กำลังทำงาน แอปพลิเคชันของคุณเป็น child process ของ shim นั้น และ shim เป็น child process ของ PID 1 ไม่มีสิ่งใดเชื่อมต่อ container เข้ากับ shell ที่ใช้สั่งเริ่มทำงาน หากคุณหยุด daemon คุณจะสูญเสีย control plane สำหรับทุก container บนเครื่อง และหากตั้งค่า live-restore เป็นปิดไว้ systemctl restart docker จะรีสตาร์ท container ของคุณไปด้วย
Podman ไม่มีกระบวนการที่เทียบเท่ากัน เมื่อเริ่ม container คุณจะได้กระบวนการ conmon (container monitor) หนึ่งรายการที่คอยดูแลกระบวนการหลักของ container โดยมีผู้ใช้ที่รันคำสั่งเป็นเจ้าของ
podman run -d --name web -p 8080:80 docker.io/library/caddy:2
ps -o user,pid,ppid,args -C conmon
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080คำสั่ง ps ควรแสดงรายการ conmon ที่ทำงานในฐานะผู้ใช้ที่คุณล็อกอินอยู่ ไม่ใช่ root และ curl ควรแสดงผลเป็น 200 เนื่องจากไม่มีบริการส่วนกลางเป็นเจ้าของ container การที่ sudo apt upgrade podman หยุดทำงานจึงไม่ส่งผลต่อสิ่งที่กำลังรันอยู่ และการที่ monitor ของ container หนึ่งตัวล้มเหลวจะไม่ทำให้ตัวอื่นหยุดทำงานตามไปด้วย
การไม่มี daemon ก็มีข้อเสียเช่นกัน ไม่มีสิ่งใดคอยเริ่ม container ให้คุณหลังจากรีบูตเครื่อง คำสั่ง --restart=always ของ Docker คือคำสัญญาที่ daemon รักษาไว้ตอนบูตเครื่อง แต่ Podman ใช้ systemd เข้ามาแทนที่ ซึ่งเป็นสิ่งที่ส่วน quadlet ด้านล่างนี้มีไว้เพื่อการนั้น
socket คืออีกครึ่งหนึ่งของเรื่องราว /var/run/docker.sock เป็น endpoint ของ API (application programming interface) ที่มี root เป็นเจ้าของ และกระบวนการใดก็ตามที่เขียนข้อมูลลงไปได้สามารถเริ่ม privileged container ที่ mount ไฟล์ระบบของ host ได้ การเพิ่มผู้ใช้เข้ากลุ่ม docker เป็นการมอบสิทธิ์ root ให้ผู้ใช้นั้นผ่านช่องทางที่อ้อมกว่า ซึ่งควรศึกษาเพิ่มเติมควบคู่ไปกับ การให้สิทธิ์บัญชีบริการเฉพาะเท่าที่จำเป็น Podman ไม่เปิดเผย socket เว้นแต่คุณจะร้องขอ และ socket ที่คุณได้รับจะเป็นของผู้ใช้รายเดียวที่ /run/user/<uid>/podman/podman.sock
การติดตั้ง Podman บน Ubuntu 24.04 และการยืนยันการทำงานแบบ rootless
sudo apt update
sudo apt install -y podman uidmap
podman --version
podman info | grep -i rootlessแพ็กเกจ uidmap มี newuidmap และ newgidmap รวมอยู่ด้วย ซึ่งเป็นตัวช่วยแบบ setuid ที่ทำให้ผู้ใช้ทั่วไปสามารถเรียกใช้ช่วง subordinate ID ได้ หากไม่มีสิ่งเหล่านี้ คอนเทนเนอร์แบบ rootless จะไม่สามารถเริ่มทำงานได้ คำสั่ง podman info ควรแสดงผลลัพธ์เป็น rootless: true
Ubuntu 24.04 มาพร้อมกับ Podman 4.9 และ Debian 13 มาพร้อมกับ Podman 5.x ซึ่งตรวจสอบข้อมูล ณ เดือนสิงหาคม 2026 ความแตกต่างของเวอร์ชันมีความสำคัญเนื่องจากไฟล์ quadlet ต้องการเวอร์ชัน 4.4 ขึ้นไป และไฟล์ quadlet แบบ .pod ต้องการเวอร์ชัน 5.0 ให้รันคำสั่ง podman --version ก่อนที่คุณจะคัดลอกตัวอย่างจากเอกสารประกอบต้นฉบับ
ผู้ใช้แบบ rootless ทุกคนจำเป็นต้องมีช่วง subordinate ID:
grep "$USER" /etc/subuid /etc/subgidผู้ใช้ที่สร้างด้วยคำสั่ง adduser บน Ubuntu จะได้รับช่วง ID โดยอัตโนมัติ แต่ผู้ใช้ที่สร้างด้วยคำสั่ง useradd -M หรือเครื่องมือตั้งค่าอื่นๆ มักจะไม่ได้รับช่วงดังกล่าว และจะเกิดข้อผิดพลาดแจ้งเตือนดังนี้:
Error: cannot find UID/GID for user deploy: no subuid ranges found for user "deploy" in /etc/subuid - check rootless mode in man pages.ให้กำหนดช่วง ID จากนั้นรีเซ็ตพื้นที่จัดเก็บข้อมูลของผู้ใช้รายนั้นเพื่อให้มีการใช้งานการแมปใหม่:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 deploy
podman system migrateอีกหนึ่งสิ่งที่มักพบในการรันครั้งแรกคือ Podman ไม่ได้ตั้งค่าเริ่มต้นให้ใช้ Docker Hub ชื่ออิมเมจแบบสั้นจะถูกตรวจสอบเทียบกับ unqualified-search-registries ในไฟล์ /etc/containers/registries.conf และในสคริปต์ที่ไม่มี terminal เชื่อมต่ออยู่ การดึงอิมเมจ (pull) จะล้มเหลวพร้อมข้อผิดพลาด short-name resolution enforced but cannot prompt without a TTY ให้ระบุชื่อเต็มทุกครั้ง และใช้ docker.io/library/nginx:1.27 แทน nginx
ประโยชน์ที่แท้จริงของ rootless containers บนเซิร์ฟเวอร์เช่า
rootless container ทำงานอยู่ภายใน user namespace ซึ่งเป็นฟีเจอร์ของ kernel ที่ช่วยให้กระบวนการ (process) มีแผนผัง user ID ส่วนตัวของตนเอง ภายใน namespace นี้ superuser ของ container จะมี UID (user ID) เป็น 0 แต่ภายนอกนั้น บน VPS ของคุณ กระบวนการเดียวกันนี้จะมีสถานะเป็นเพียงผู้ใช้งานทั่วไปที่ล็อกอินอยู่ ดังนั้น root ภายใน container จึงไม่ใช่ root บนเครื่อง host
นี่คือขอบเขตของความปลอดภัยที่ได้รับอย่างแท้จริง ไม่ว่าจะเป็น image ที่บังคับให้รันในฐานะ root, เว็บแอปพลิเคชันที่มีช่องโหว่ remote code execution หรือการหลบหนีออกจาก container (escape) ที่ต้องอาศัยการเป็น UID 0 จากภายนอก ทั้งหมดนี้จะจบลงที่การได้รับสิทธิ์เพียงผู้ใช้งานทั่วไปของคุณแทนที่จะเป็นสิทธิ์ระดับเครื่อง สิ่งที่ rootless ไม่ได้ทำคือการป้องกันคุณจากช่องโหว่ของ kernel และไม่ได้ปกป้องไฟล์ของคุณเอง เนื่องจากกระบวนการที่หลบหนีออกมาจะรันในฐานะตัวคุณและสามารถอ่านไฟล์ทุกอย่างที่คุณอ่านได้
Docker สามารถรันแบบ rootless ได้เช่นกัน dockerd-rootless-setuptool.sh install จะตั้งค่า daemon แยกตามผู้ใช้งานแต่ละรายซึ่งทำงานได้ดี ความแตกต่างอยู่ที่ค่าเริ่มต้นที่ระบบเลือกใช้ สำหรับ Podman คุณจะได้ใช้งานแบบ rootless โดยไม่ต้องร้องขอ ดังนั้นความล้มเหลวแรกที่คุณอาจพบคือ container ไม่สามารถ bind พอร์ต 80 ได้ แทนที่จะเป็นบริการที่รันในฐานะ root อย่างเงียบๆ มาตลอดสองปี
เหตุใดไฟล์ใน volume ของฉันจึงมีเจ้าของเป็น UID 100999
เป็นเพราะ user namespace เดียวกันนั้น UID 0 ในคอนเทนเนอร์จะถูกแมปไปยัง UID บนโฮสต์ของคุณ ส่วน UID 1 ในคอนเทนเนอร์จะถูกแมปไปยัง ID แรกในช่วง subuid ของคุณและนับต่อขึ้นไปเรื่อยๆ ในกรณีที่ช่วงเริ่มต้นที่ 100000 ตัว UID 1000 ของคอนเทนเนอร์จะปรากฏบนโฮสต์เป็น 100999
mkdir -p "$PWD/data"
podman run --rm --user 1000 -v "$PWD/data:/data" docker.io/library/alpine:3 sh -c 'id -u; touch /data/f'
ls -ln "$PWD/data"คอนเทนเนอร์แสดงผลเป็น 1000 แต่รายการบนโฮสต์แสดงเจ้าของเป็น 100999 เนื่องจาก 100000 บวก 1000 ลบ 1 เท่ากับ 100999 ไม่มีสิ่งใดเสียหาย และการใช้ chown ปกติจะไม่สามารถแก้ไขปัญหานี้ได้ เนื่องจากผู้ใช้ที่ไม่มีสิทธิ์ (unprivileged user) ของคุณไม่สามารถเปลี่ยนความเป็นเจ้าของไฟล์ภายนอก namespace ได้เลย
มี 4 วิธีในการแก้ไข:
podman unshare chown 1000:1000 "$PWD/data"ใช้รันคำสั่ง chown ภายใน user namespace เดียวกัน ซึ่งตัวเลขจะมีความหมายตรงกับที่คอนเทนเนอร์เข้าใจ-v "$PWD/data:/data:U"เป็นการสั่งให้ Podman แก้ไขความเป็นเจ้าของของไดเรกทอรีต้นทางให้คุณโดยอัตโนมัติ ควรใช้กับไดเรกทอรีใหม่เท่านั้น ไม่ควรใช้กับข้อมูลที่คุณต้องการรักษาไว้--userns=keep-idเป็นการแมป UID บนโฮสต์ของคุณให้ตรงกับ UID ภายในคอนเทนเนอร์ เพื่อให้ไฟล์ใหม่ที่สร้างขึ้นมีคุณเป็นเจ้าของ- การใช้ named volume เช่น
-v appdata:/dataจะช่วยเลี่ยงปัญหานี้ได้ เพราะ Podman จะสร้าง volume ไว้ในพื้นที่จัดเก็บของคุณโดยกำหนดความเป็นเจ้าของให้ถูกต้องตั้งแต่ต้น
หากคุณเคยประสบปัญหานี้ใน Docker มันคือปัญหาเดียวกันที่เพิ่มขึ้นมาอีกหนึ่งชั้น ตัวแปร PUID และ PGID ที่ image จำนวนมากกำหนดไว้ จะเป็นการตั้งค่า UID ที่กระบวนการภายในคอนเทนเนอร์จะใช้ และภายใต้ rootless Podman ตัว UID นั้นจะถูกแมปซ้ำเป็นครั้งที่สอง ดังนั้นการใช้ PUID=1000 ภายในคอนเทนเนอร์แบบ rootless ก็ยังคงเขียนไฟล์ลงบนโฮสต์โดยมีเจ้าของเป็น 100999 อยู่ดี ให้เลือกตัวเลขโดยคำนึงถึงการแมปครั้งที่สองนี้ หรือย้ายข้อมูลไปไว้ใน named volume แล้วเลิกกังวลเรื่องนี้ไปได้เลย
ข้อควรทราบเพิ่มเติมเกี่ยวกับการ mount: แฟล็ก :z และ :Z ที่คุณเห็นในตัวอย่างของ Fedora และ RHEL คือตัวเลือกสำหรับการทำ SELinux relabel ซึ่งใน Ubuntu จะใช้ AppArmor ดังนั้นแฟล็กเหล่านี้จึงไม่มีผลใดๆ นอกจากนี้ rootless Podman ไม่สามารถ mount ไดเรกทอรีบนโฮสต์ที่ผู้ใช้ของคุณไม่มีสิทธิ์อ่านได้ ซึ่งถือเป็นจุดประสงค์ด้านความปลอดภัยไม่ใช่ข้อผิดพลาดของระบบ
เหตุใด Podman แบบ rootless ถึงไม่ยอมเปิดพอร์ต 80
เนื่องจากการผูกพอร์ตที่ต่ำกว่า 1024 ต้องใช้สิทธิ์ที่ผู้ใช้ทั่วไปไม่มี ข้อความแสดงข้อผิดพลาดจะระบุวิธีแก้ไขไว้ดังนี้:
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission deniedมีสองวิธีที่ใช้ได้ วิธีแรกคือการลดเกณฑ์สำหรับทั้งโฮสต์:
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-podman.conf
sudo sysctl --system
sysctl net.ipv4.ip_unprivileged_port_startคำสั่งสุดท้ายควรแสดงผลลัพธ์กลับมาเป็น 80 โปรดทำความเข้าใจว่าการตั้งค่านี้ส่งผลอย่างไร: ผู้ใช้ทุกคนบนเครื่องจะสามารถผูกพอร์ต 80 และ 443 ได้ ไม่ใช่แค่ผู้ใช้ที่รันคอนเทนเนอร์เท่านั้น บน VPS ที่มีผู้ดูแลคนเดียวถือเป็นข้อแลกเปลี่ยนที่ยอมรับได้ แต่บนเครื่องที่มีบัญชีผู้ใช้อื่นใช้งานอยู่ไม่ควรทำเช่นนี้ อีกวิธีหนึ่งคือการเปิดพอร์ต 8080 แล้วใช้ reverse proxy วางไว้ด้านหน้า ซึ่งเป็นจุดที่คุณต้องการให้ ออกและต่ออายุใบรับรองโดย certbot บน nginx อยู่แล้ว
การเปิดพอร์ตแบบ rootless ยังเปลี่ยนสิ่งที่แอปพลิเคชันของคุณมองเห็นด้วย Podman 4.x ใช้ slirp4netns ร่วมกับตัวจัดการพอร์ต rootlesskit เป็นค่าเริ่มต้น ทำให้การเชื่อมต่อที่ส่งต่อเข้ามาถูกเขียนที่อยู่ต้นทางใหม่ ส่งผลให้ access log บันทึกผู้เข้าชมทุกคนเป็น 10.0.2.100 ส่วน Podman 5.0 เปลี่ยนค่าเริ่มต้นเป็น pasta ซึ่งจะคงที่อยู่จริงของไคลเอนต์ไว้ สำหรับเวอร์ชัน 4.x การใช้ --network slirp4netns:port_handler=slirp4netns จะช่วยกู้คืนที่อยู่ต้นทางที่แท้จริงกลับมาได้ แต่ต้องแลกกับประสิทธิภาพการรับส่งข้อมูลที่ลดลงบ้าง
มีข้อดีที่น่าประหลาดใจอย่างหนึ่งคือ พอร์ตที่เปิดแบบ rootless จะเป็นซ็อกเก็ตที่รอรับการเชื่อมต่อตามปกติซึ่งเป็นเจ้าของโดยโพรเซสของผู้ใช้ทั่วไป ดังนั้นกฎ firewall input ของคุณจึงมีผลกับพอร์ตเหล่านี้ ในขณะที่ Docker เปิดพอร์ตโดยการเขียนกฎ NAT (network address translation) รวมกับกฎ forwarding accept ของตัวเอง ซึ่งเป็นเหตุผลว่าทำไม พอร์ตที่เปิดโดย Docker ถึงเพิกเฉยต่อกฎ ufw ที่คุณคิดว่าบล็อกไว้ Podman แบบ rootful ก็ใช้กลไกที่คล้ายกันและได้รับผลกระทบจากกับดักนี้เช่นกัน แต่แบบ rootless ไม่เป็นเช่นนั้น
ไฟล์ Docker Compose ของฉันยังใช้งานได้บน Podman หรือไม่
โดยส่วนใหญ่แล้วใช้งานได้ โดยมีสองแนวทางที่แตกต่างกัน แนวทางแรกคือ podman-compose ซึ่งเป็นการติดตั้งแยกต่างหากที่อ่านไฟล์เดียวกันและสั่งงานผ่าน Podman CLI:
sudo apt install -y podman-compose
podman-compose up -d
podman psแนวทางที่สองคือการใช้ Docker Compose จริงๆ เพื่อสื่อสารกับ Docker-compatible API ของ Podman ผ่าน socket ประจำตัวผู้ใช้:
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix:///run/user/$(id -u)/podman/podman.sock"
docker compose up -d
podman psdocker compose ps และ podman ps ควรแสดงรายการคอนเทนเนอร์ชุดเดียวกัน เนื่องจากมีคอนเทนเนอร์เพียงชุดเดียวเท่านั้น การแก้ไขชื่อ (name resolution) ก็ทำงานได้เช่นกัน โดย backend เครือข่ายเริ่มต้นของ Podman คือ netavark จะรัน aardvark-dns ทำให้คอนเทนเนอร์ที่อยู่บนเครือข่ายที่ผู้ใช้กำหนดเองสามารถค้นหากันและกันได้ด้วยชื่อ
อย่างไรก็ตาม ยังมีข้อจำกัดที่ต้องระวัง สิ่งใดก็ตามที่ mount /var/run/docker.sock จะต้องชี้ไปยัง socket ของ Podman หรือต้องตัดออก network_mode: host จะทำงานแตกต่างออกไปภายใต้ user namespace ส่วน depends_on ที่ใช้ร่วมกับ condition: service_healthy นั้นรองรับไม่เท่ากันในแต่ละเวอร์ชันของ podman-compose และ restart: always จะไม่คงอยู่หลังจากการรีบูตด้วยตัวมันเอง ซึ่งส่วนถัดไปจะมาแก้ไขปัญหานี้ Compose ยังคงเป็นวิธีที่ดีในการอธิบาย ชุดคอนเทนเนอร์หลายตัวในไฟล์เดียว และภายใต้ Podman มันทำหน้าที่เป็นชั้นการแปล (translation layer) สำหรับชุดบริการที่คุณตั้งใจจะใช้งานเป็นเวลาหลายปี ควรแปลงเป็น quadlets เพื่อรักษา abstraction เพียงชุดเดียวแทนที่จะเป็นสองชุด
Pods: แนวคิดที่ Docker ไม่มีคำตอบให้
Pod คือกลุ่มของคอนเทนเนอร์ที่ใช้ network namespace ร่วมกัน Podman จะเริ่มคอนเทนเนอร์ขนาดเล็ก infra เพื่อคง namespace นั้นไว้ให้เปิดอยู่ จากนั้นสมาชิกในกลุ่มจะสามารถติดต่อกันได้ผ่าน 127.0.0.1 โดยไม่ต้องอาศัย network ที่ผู้ใช้กำหนดเองหรือการทำ service discovery ใดๆ
podman pod create --name app -p 8080:80
podman run -d --pod app --name app-cache docker.io/library/redis:7
podman run -d --pod app --name app-web docker.io/library/nginx:1.27
podman pod ps
podman ps --podpodman pod ps ควรแสดง Pod Running ที่มีคอนเทนเนอร์อยู่ 3 ตัว โดยนับรวมคอนเทนเนอร์โครงสร้างพื้นฐาน (infra container) เข้าไปด้วย ขณะนี้คอนเทนเนอร์เว็บสามารถเข้าถึง Redis ได้ที่ 127.0.0.1:6379 แทนที่จะเป็น app-cache:6379 กฎสองข้อที่ตามมาจากการใช้ namespace ร่วมกันคือ ให้ประกาศพอร์ตที่ระดับ Pod เท่านั้นห้ามประกาศที่ระดับสมาชิก และสมาชิกสองตัวใดๆ จะไม่สามารถฟัง (listen) บนพอร์ตเดียวกันได้
นี่คือโมเดลของ Kubernetes ซึ่ง Podman ได้นำมาปรับใช้ podman kube generate app > app.yaml จะเขียน Kubernetes manifest จากสิ่งที่กำลังทำงานอยู่ (แพ็กเกจรุ่นเก่าจะใช้คำสั่ง podman generate kube) และ podman kube play app.yaml จะสร้างมันขึ้นมาใหม่บนโฮสต์อื่น Quadlet มี unit type แบบ .kube ซึ่งรันไฟล์ดังกล่าวในฐานะ systemd service นี่เป็นวิธีจัดกลุ่มบริการที่แตกต่างอย่างแท้จริง และเป็นเหตุผลที่หนักแน่นที่สุดในการเลือกใช้ Podman หากคุณมีแผนที่จะใช้งาน Kubernetes ในอนาคต
การเริ่มทำงานอัตโนมัติโดยไม่ต้องใช้ daemon: quadlet units
Quadlet คือ systemd generator ซึ่งจะเปลี่ยนไฟล์คำอธิบาย container ขนาดสั้นให้กลายเป็น systemd service จริงในขณะบูตระบบ ไฟล์เหล่านี้จะถูกเก็บไว้ใน ~/.config/containers/systemd/ สำหรับผู้ใช้แบบ rootless หรือ /etc/containers/systemd/ สำหรับ root
~/.config/containers/systemd/caddy.container:
[Unit]
Description=Caddy web server
After=network-online.target
[Container]
Image=docker.io/library/caddy:2
ContainerName=caddy
PublishPort=8080:80
Volume=caddy-data.volume:/data
Environment=TZ=UTC
AutoUpdate=registry
[Service]
Restart=always
MemoryMax=512M
[Install]
WantedBy=default.target~/.config/containers/systemd/caddy-data.volume สามารถเว้นว่างไว้เกือบทั้งหมดได้ เนื่องจากส่วนหัวของ section คือสิ่งที่สร้าง volume ขึ้นมา:
[Volume]systemctl --user daemon-reload
systemctl --user start caddy
systemctl --user status caddy
journalctl --user -u caddy -n 50ชื่อของ service จะมาจากชื่อไฟล์: caddy.container จะกลายเป็น caddy.service ห้ามรัน systemctl --user enable caddy เนื่องจาก unit ที่ถูกสร้างขึ้นโดยอัตโนมัติไม่สามารถสั่ง enable ได้ และ systemd จะตอบกลับว่า Failed to enable unit: Unit /run/user/1000/systemd/generator/caddy.service is transient or generated. ส่วน [Install] คือส่วนที่เริ่มการทำงานของ container ขณะบูต และ daemon-reload คือคำสั่งที่ใช้สร้าง unit ใหม่หลังจากที่คุณแก้ไขไฟล์
ต่อไปคือการตั้งค่าที่มักทำให้เกิดปัญหาบ่อยที่สุด:
sudo loginctl enable-linger deploy
loginctl show-user deploy --property=Lingerคาดหวังผลลัพธ์เป็น Linger=yes หากไม่มีการตั้งค่า linger ระบบ systemd จะปิด session ของผู้ใช้ทั้งหมดทันทีเมื่อการเชื่อมต่อ SSH ครั้งสุดท้ายของคุณสิ้นสุดลง ส่งผลให้ container แบบ rootless ทุกตัวหยุดทำงานและไม่กลับมาเริ่มใหม่ขณะบูตระบบ ปัญหา container หายไปเมื่อคุณ logout มักเกิดจากสาเหตุนี้เสมอ
เนื่องจาก container คือกระบวนการหลักของ service unit ทั่วไป การควบคุมของ systemd จึงมีผลโดยตรง MemoryMax= และ CPUQuota= ใน section [Service] จะทำงานเช่นเดียวกับ service อื่นๆ ที่คุณจำกัดทรัพยากรด้วย systemd การทำงานนี้ต้องใช้ cgroup v2 (control group version 2) ซึ่ง Ubuntu ใช้เป็นค่าเริ่มต้นตั้งแต่เวอร์ชัน 22.04 เป็นต้นมา ตรวจสอบได้ด้วย podman info | grep -i cgroup
การอัปเดตมีกลไกที่สอดคล้องกัน AutoUpdate=registry ร่วมกับ systemctl --user enable --now podman-auto-update.timer จะตรวจสอบ registry เพื่อหา image เวอร์ชันใหม่ใน tag เดียวกัน จากนั้นจะรีสตาร์ท unit และย้อนกลับไปใช้ image เดิมหาก container ใหม่ไม่สามารถเริ่มทำงานได้ ให้รัน podman auto-update --dry-run ก่อนเพื่อดูว่ามีการเปลี่ยนแปลงใดบ้าง คำสั่ง podman generate systemd แบบเก่าที่ยังคงมีอยู่ถือว่าเลิกใช้งานแล้ว ดังนั้นควรเขียน quadlets สำหรับงานใหม่ทั้งหมดแทน
ขอบเขตการใช้งาน docker alias และข้อจำกัด
sudo apt install -y podman-docker
sudo touch /etc/containers/nodocker
docker pspodman-docker จะติดตั้ง wrapper ของ /usr/bin/docker ซึ่งเรียกใช้งาน Podman หากไม่มีไฟล์ nodocker ทุกคำสั่งที่เรียกใช้จะแสดงข้อความ Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. ออกมาก่อนเสมอ wrapper นี้ครอบคลุมคำสั่งที่คุณใช้งานเป็นประจำ ได้แก่ run, ps, logs, exec, build, pull, push, inspect, cp, volume, network
ส่วนที่ไม่สามารถใช้งานร่วมกันได้นั้นมีรายการที่สั้นกว่าแต่มีความสำคัญมากกว่า Swarm mode ไม่มีฟังก์ชันการทำงานที่เทียบเท่ากัน ดังนั้น Swarm stack จึงไม่สามารถทำงานได้ เครื่องมือที่สื่อสารผ่าน Docker socket จำเป็นต้องมีการ export Podman socket ออกมา และบางเครื่องมือยังคงตรวจพบความแตกต่างได้ เช่น Traefik Docker provider จะทำงานได้เมื่อชี้ไปยัง /run/user/<uid>/podman/podman.sock ในขณะที่ Watchtower ไม่สามารถใช้งานได้เลยเนื่องจาก podman auto-update ทำหน้าที่นั้นแทน นอกจากนี้ พื้นที่จัดเก็บข้อมูลยังแยกจากกัน Podman จึงไม่สามารถมองเห็น image ที่คุณดึงมาด้วย Docker ไว้ก่อนหน้านี้ได้ และ podman images บนโฮสต์ที่ใช้งาน Docker อยู่เดิมจะเริ่มต้นด้วยสถานะว่างเปล่า
การย้าย stack ที่กำลังทำงานอยู่ทีละขั้นตอน
- สร้างหรือเลือกผู้ใช้ที่ไม่มีสิทธิ์ root (unprivileged user) เพื่อเป็นเจ้าของคอนเทนเนอร์ และยืนยันว่าผู้ใช้นั้นมีช่วง UID ใน
/etc/subuid - ดึงอิมเมจที่มาจาก registry ใหม่อีกครั้งโดยใช้ชื่อแบบเต็ม (fully qualified names) เนื่องจาก Podman มีที่เก็บอิมเมจแยกต่างหากและไม่สามารถอ่านข้อมูลจาก Docker ได้
- ย้ายอิมเมจที่สร้างขึ้นในเครื่องข้ามไปยังระบบใหม่โดยใช้
docker save app:1.4 | podman load - หยุดการทำงานของคอนเทนเนอร์ Docker คัดลอกเนื้อหาของแต่ละ volume ออกจาก
/var/lib/docker/volumes/<name>/_dataจากนั้นแก้ไขสิทธิ์ความเป็นเจ้าของไฟล์ด้วยpodman unshare chown -R 1000:1000 <path> - จัดการเรื่องพอร์ต: ให้เปิดพอร์ตที่สูงกว่า 1024 ไว้หลัง reverse proxy หรือตั้งค่า
net.ipv4.ip_unprivileged_port_start - เขียนไฟล์ quadlet หนึ่งไฟล์ต่อหนึ่งคอนเทนเนอร์ รันคำสั่ง
systemctl --user daemon-reloadและเริ่มการทำงานของแต่ละบริการ - รันคำสั่ง
sudo loginctl enable-linger <user>รีบูต VPS ล็อกอินกลับเข้ามา และตรวจสอบว่าpodman psแสดงรายการบริการทั้งหมดอีกครั้ง
เอนจินทั้งสองตัวไม่มีส่วนใดที่ใช้ร่วมกัน ทั้งที่เก็บอิมเมจและเครือข่ายแยกจากกันโดยสิ้นเชิง ดังนั้นคุณสามารถรันทั้งสองระบบไปพร้อมกันในระหว่างการย้ายได้ โดยสิ่งที่อาจเกิดความขัดแย้งกันมีเพียงหมายเลขพอร์ตของโฮสต์เท่านั้น ให้ย้ายทีละบริการ ตรวจสอบการทำงานเป็นเวลาหนึ่งวัน แล้วจึงย้ายบริการถัดไป
Podman กับ Docker: ตัวไหนที่เหมาะกับ VPS ของคุณ?
ให้ใช้ Docker ต่อไปหาก stack ของคุณอยู่ในไฟล์ compose ที่มีผู้อื่นร่วมดูแลด้วย หรือหากคุณต้องพึ่งพาเครื่องมือที่สื่อสารผ่าน Docker socket ความเข้ากันได้กับสิ่งที่คนอื่นเขียนถือเป็นฟีเจอร์ที่สำคัญ และ Docker ก็มีความเข้ากันได้มากกว่า ทีมที่ทุกคนในทีมใช้ Docker บนแล็ปท็อปจะได้รับประโยชน์ที่เป็นรูปธรรมจากการใช้ engine เดียวกันในสภาพแวดล้อม production
ให้เปลี่ยนไปใช้ Podman หาก VPS ของคุณรันบริการเพียงไม่กี่อย่างที่คุณควบคุมแบบเบ็ดเสร็จ หรือหากคุณต้องการให้แต่ละแอปพลิเคชันรันภายใต้ผู้ใช้ที่ไม่มีสิทธิ์ (unprivileged user) โดยไม่มีกลุ่ม docker อยู่บนเครื่องเลย ความสอดคล้องกับ distribution ก็เป็นปัจจัยสำคัญเช่นกัน: RHEL และรุ่นที่สร้างใหม่จาก RHEL ใช้ Podman เป็น engine ที่ได้รับการสนับสนุน ดังนั้นบนระบบเหล่านั้น Podman จึงเป็นเส้นทางที่พบปัญหาประหลาดใจน้อยกว่า หากคุณดูแลทุกอย่างด้วย systemd units อยู่แล้ว quadlets จะให้ความรู้สึกเหมือนชิ้นส่วนที่ขาดหายไปได้ถูกเติมเต็ม มากกว่าจะเป็นเครื่องมือใหม่ที่ต้องเรียนรู้
มีตัวเลือกตรงกลางอีกหนึ่งอย่างที่ควรกล่าวถึง Rootful Podman มีพฤติกรรมคล้ายกับ Docker โดยยังคงใช้คำสั่ง docker ผ่าน wrapper และยังคงกำจัด daemon ที่ต้องรันค้างไว้ตลอดเวลาออกไป อย่างไรก็ตาม วิธีนี้จะสูญเสียข้อดีเรื่องการรันแบบไร้สิทธิ์ root (rootless) ซึ่งเป็นส่วนที่เปลี่ยนสถานะความปลอดภัยของคุณ ดังนั้นให้ถือว่าวิธีนี้เป็นเพียงจุดแวะพักระหว่างทางเท่านั้น
หากคุณกำลังสร้าง container host เครื่องแรก เส้นทางการติดตั้งและเพิ่มความปลอดภัยให้กับ Docker บน VPS ใหม่ เป็นเส้นทางที่สั้นกว่า และความรู้ทั้งหมดนั้นจะไม่สูญเปล่า เนื่องจาก image และ volume เป็นออบเจกต์เดียวกันในทั้งสอง engine การย้ายในภายหลังจะเปลี่ยนเพียงวิธีการดูแลบริการของคุณเท่านั้น โดยแทบจะไม่กระทบส่วนอื่นเลย
FAQ
Podman เป็นตัวแทนที่ใช้งานแทน Docker ได้ทันทีเลยหรือไม่
ในแง่ของคำสั่งที่ใช้พิมพ์ถือว่าใกล้เคียงกันมาก การติดตั้ง podman-docker จะให้ wrapper ชื่อ /usr/bin/docker มาใช้งาน และคำสั่งอย่าง run, ps, build, logs และ exec ก็ทำงานในลักษณะเดียวกัน อย่างไรก็ตาม Podman ไม่ใช่ตัวแทนของ daemon โดยตรง โดยไม่มีฟีเจอร์เทียบเท่า Swarm และเครื่องมือที่เชื่อมต่อกับ /var/run/docker.sock จะต้องเปลี่ยนไปชี้ที่ socket ของ Podman ในระดับผู้ใช้แทน นอกจากนี้ image ที่ดึงมาด้วย Docker จะไม่ปรากฏใน Podman เนื่องจากทั้งสองระบบแยกพื้นที่จัดเก็บข้อมูลออกจากกัน
ทำไม container แบบ rootless ของ Podman ถึงหยุดทำงานเมื่อฉันออกจากระบบ SSH
เพราะ systemd จะหยุด session ของผู้ใช้และ service ทั้งหมดที่เกี่ยวข้องเมื่อการล็อกอินครั้งสุดท้ายสิ้นสุดลง ให้รันคำสั่ง sudo loginctl enable-linger <user> จากนั้นตรวจสอบว่า loginctl show-user <user> --property=Linger แสดงผลเป็น Linger=yes หรือไม่ การตั้งค่า linger จะช่วยให้ instance ของ systemd ของผู้ใช้นั้นยังคงทำงานอยู่แม้ไม่มี session ที่ใช้งานอยู่ ซึ่งเป็นวิธีที่ทำให้ container เริ่มทำงานใหม่โดยอัตโนมัติหลังจาก reboot เซิร์ฟเวอร์
ทำไมไฟล์ใน volume ของฉันถึงมีเจ้าของเป็น UID 100999
Rootless Podman จะ map UID 0 ของ container เข้ากับผู้ใช้บน host ของคุณ จากนั้นจะ map UID 1 ขึ้นไปเข้ากับช่วง subuid ของคุณ หากช่วงดังกล่าวเริ่มต้นที่ 100000 ค่า UID 1000 ใน container จึงกลายเป็น 100999 บน host คุณสามารถแก้ไขปัญหานี้จากภายใน namespace ด้วย podman unshare chown 1000:1000 /path/to/data, mount ด้วย flag :U ในการรันครั้งแรก หรือใช้ --userns=keep-id เพื่อให้ UID ใน container ตรงกับ UID ของคุณ
ฉันยังสามารถใช้ docker-compose.yml กับ Podman ได้หรือไม่
ได้ โดยทำได้สองวิธี วิธีแรกคือใช้ podman-compose เพื่ออ่านไฟล์และสั่งงาน Podman CLI โดยตรง หรือวิธีที่สองคือเปิดใช้งาน compatibility socket ด้วย systemctl --user enable --now podman.socket, ตั้งค่า DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock แล้วรัน docker compose จริงๆ เข้ากับ socket นั้น ทั้งนี้อาจพบปัญหาในการใช้งาน network_mode: host, บริการที่ต้อง mount Docker socket และ restart: always ซึ่งจำเป็นต้องใช้ quadlet unit และการตั้งค่า linger เพื่อให้ทำงานต่อได้หลัง reboot
การรันแบบ rootless ทำให้ container ปลอดภัยขึ้นจริงหรือไม่
การรันแบบ rootless ช่วยลดความเสี่ยงเฉพาะจุดหนึ่งคือ หากกระบวนการใดหลุดออกจาก container ได้ กระบวนการนั้นจะมีสิทธิ์เพียงเท่ากับผู้ใช้ทั่วไปของคุณ ไม่ใช่สิทธิ์ระดับ root ซึ่งเป็นข้อดีที่สำคัญและเป็นเหตุผลว่าทำไมกลุ่ม docker ที่มีสิทธิ์เทียบเท่า root จึงไม่มีใน Podman แบบ rootless อย่างไรก็ตาม วิธีนี้ไม่สามารถป้องกันช่องโหว่ของ kernel และไม่สามารถปกป้องไฟล์ที่ผู้ใช้ของคุณเข้าถึงได้ ดังนั้นคุณยังคงต้องใช้มาตรการรักษาความปลอดภัยอื่นๆ ตามปกติเหมือนที่ทำกับเซิร์ฟเวอร์ทั่วไป