อัปเกรด Docker Engine บนเซิร์ฟเวอร์ที่ใช้งานจริง
การอัปเกรด Docker Engine บนเซิร์ฟเวอร์ที่กำลังรันคอนเทนเนอร์กว่าร้อยตัวต้องเตรียมการอย่างรอบคอบ เมื่อถึงเวลาย้ายจาก Docker Engine 28.1 เป็น 28.2 และ Docker Compose 2.35 เป็น 2.36 บนเซิร์ฟเวอร์ใช้งานจริงของ Hetzner เราใช้วินัยเดียวกับการย้ายฐานข้อมูล: มีรายการเตรียมพร้อมเป็นลายลักษณ์อักษร ทดสอบแนวทางย้อนกลับ กำหนดลำดับดำเนินการอย่างชัดเจน และตรวจสอบหลังอัปเกรดก่อนประกาศจบช่วงบำรุงรักษา
ต่อไปนี้คือรายละเอียดทั้งหมดของแนวทางที่เราใช้ ทั้งเหตุผลของแต่ละขั้น คำสั่งที่รันจริง และข้อควรระวังเมื่อจำนวนคอนเทนเนอร์มากจนการกำหนดนโยบายเริ่มใหม่ผิดอาจทำให้เกิดปัญหาต่อเนื่องและกู้คืนยาก
เหตุใดการอัปเกรดนี้จึงต้องมีคู่มือขั้นตอนปฏิบัติ
Docker Engine 28.2 เพิ่มตัวเลือกเครือข่าย bridge และตัวเลือกของ daemon สำหรับการกำหนดเส้นทางตรงไปยังคอนเทนเนอร์ และยกเลิกการรองรับการ pull อิมเมจ schema 1 แบบเก่า ในช่วงบำรุงรักษาเดียวกัน ปลั๊กอิน Compose เปลี่ยนจาก 2.35.1 เป็น 2.36.0 แม้เป็นการออกรุ่นย่อย แต่การเปลี่ยนแปลงวิธีเริ่มชุดระบบก็ควรทดสอบ โดยเฉพาะบล็อก depends_on ที่ใช้ condition: service_healthy
บนเซิร์ฟเวอร์ที่ไฟล์ docker-compose.yml เดียวอาจควบคุมบริการที่พึ่งพากันสิบสองตัว ทั้งฐานข้อมูล reverse proxy คอนเทนเนอร์แอป และงานเบื้องหลัง การเปลี่ยนพฤติกรรมการจัดการการพึ่งพากันโดยไม่แจ้งอาจไม่แสดงปัญหาทันที แต่ปรากฏเวลา 03:00 เมื่อการตรวจสอบสถานะไม่ทำงานและคอนเทนเนอร์ที่รออยู่วนซ้ำไม่จบ
ทางแก้ที่ใช้ได้จริงคือเตรียมให้พร้อม ไม่ใช่ระวังโดยไม่มีเหตุผล
รายการตรวจสอบก่อนอัปเกรด
ก่อนแก้แพ็กเกจใด เราตรวจรายการต่อไปนี้ ทุกข้อมีเหตุผลรองรับ
1. ทำรายการคอนเทนเนอร์ที่รันอยู่ทั้งหมดและนโยบายเริ่มใหม่
docker ps --format "table {{.Names}} {{.Status}} {{.Image}}" | sort
docker inspect $(docker ps -q) --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}' | sort
รายการนี้เป็นค่าอ้างอิงตั้งต้น คอนเทนเนอร์ที่ใช้ restart: always จะพยายามกลับมาทำงานเองเมื่อ Docker daemon เริ่มใหม่ ซึ่งเป็นสิ่งที่ต้องการ แต่ก็ต่อเมื่อไฟล์ Compose และอิมเมจที่ใช้ได้รับการตรวจแล้วว่าอยู่ในสถานะที่ใช้งานได้ คอนเทนเนอร์ที่วนหยุดและเริ่มใหม่ขณะอัปเกรดจะยังมีปัญหาหลังอัปเกรด จึงควรรู้ล่วงหน้า
2. ตรวจสอบไฟล์ Compose ทั้งหมดก่อนอัปเกรด
docker compose config --quiet 2>&1 | grep -i warning
รันคำสั่งนี้ในทุกไดเรกทอรีที่มี docker-compose.yml คำสั่งนี้ใช้ Compose เวอร์ชันที่ติดตั้งอยู่ จึงแสดงคำเตือนที่มีอยู่แล้ว ไม่ได้ยืนยันความเข้ากันได้กับเวอร์ชันเป้าหมาย หากต้องการยืนยัน ให้ทดสอบเวอร์ชันเป้าหมายในสภาพแวดล้อมทดสอบก่อนใช้งานจริง Compose รุ่นปัจจุบันเตือนเกี่ยวกับคีย์ version: ที่ล้าสมัยด้านบนของไฟล์ Compose (คีย์นี้ถูกข้าม) ส่วนคำสั่ง links: แบบเก่าไม่จำเป็นอีกต่อไป เพราะบริการบนเครือข่ายเดียวกันเข้าถึงกันได้ด้วยชื่อ คำเตือนอาจกลายเป็นข้อผิดพลาดในรุ่นถัดไป จึงควรแก้ตอนนี้
3. ตรวจสอบพื้นที่ดิสก์ที่เหลือ
df -h /var/lib/docker
docker system df
การอัปเกรดจะดึง containerd shim ใหม่และแทนไบนารี Docker Engine แต่ไบนารีเก่ากับแพ็กเกจที่พึ่งพาอาจไม่ถูกล้างอัตโนมัติ บนเซิร์ฟเวอร์ที่ใช้งานมาหนึ่งปี ผลจาก docker system df มักแสดงชั้นอิมเมจที่เรียกคืนพื้นที่ได้หลายกิกะไบต์ ล้างก่อนอัปเกรด ไม่ใช่ระหว่างอัปเกรด
docker system prune -f --volumes
ใช้ --volumes เฉพาะเมื่อแน่ใจว่าไม่มีวอลุ่มไม่ระบุชื่อเก็บข้อมูลที่ต้องใช้ เซิร์ฟเวอร์ของเราเมานต์ข้อมูลถาวรทั้งหมดจากวอลุ่มมีชื่อที่ประกาศไว้อย่างชัดเจน จึงใช้ได้อย่างปลอดภัย
4. บันทึกเวอร์ชัน Docker Engine และ Compose ปัจจุบันลงไฟล์อ้างอิง
docker version > /root/docker-pre-upgrade.txt
docker compose version >> /root/docker-pre-upgrade.txt
docker ps -a >> /root/docker-pre-upgrade.txt
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin >> /root/docker-pre-upgrade.txt
ขั้นตอนนี้ใช้เวลาสิบวินาที และให้ข้อมูลอ้างอิงสำหรับย้อนกลับอย่างชัดเจนเมื่อเกิดปัญหาและต้องระบุว่าคอนเทนเนอร์ใดรันอยู่ขณะอัปเกรด
5. แจ้งระบบปลายทาง
เซิร์ฟเวอร์ของเรารันชุดระบบ Supabase ที่ติดตั้งและดูแลเองกับ n8n หลายอินสแตนซ์สำหรับเวิร์กโฟลว์อัตโนมัติ webhook ใดที่ทำงานระหว่างรอบการเริ่มคอนเทนเนอร์ใหม่จะล้มเหลว และหากผู้ส่งไม่ลองส่งใหม่ เหตุการณ์นั้นอาจสูญหายโดยไม่มีใครสังเกตเห็น เรากำหนดช่วงบำรุงรักษาในเครื่องมือเฝ้าติดตามความพร้อมใช้งาน และปิด webhook ขาเข้าของ n8n ตลอดช่วงนั้น
แผนย้อนกลับไปเวอร์ชันเดิม
แผนย้อนกลับมีประโยชน์ก็ต่อเมื่อตัดสินเงื่อนไขที่จะใช้ไว้ล่วงหน้า เงื่อนไขของเราตรงไปตรงมา: หากคอนเทนเนอร์ใดที่รันก่อนอัปเกรดยังไม่รันหลังอัปเกรดเสร็จสิบนาที และกู้คืนด้วย docker compose up -d ไม่ได้ เราจะย้อนกลับไบนารี Docker Engine
การย้อนกลับ Docker Engine บนระบบตระกูล Debian ต้องระบุให้ติดตั้งแพ็กเกจเวอร์ชันเดิม เราบันทึกสตริงเวอร์ชันที่แน่นอนไว้ก่อนเริ่ม:
apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin
ผลลัพธ์บนเซิร์ฟเวอร์ของเราก่อนอัปเกรด:
docker-ce:
Installed: 5:28.1.1-1~debian.12~bookworm
Candidate: 5:28.2.2-1~debian.12~bookworm
docker-compose-plugin:
Installed: 2.35.1-1~debian.12~bookworm
Candidate: 2.36.0-1~debian.12~bookworm
คำสั่งย้อนกลับหากจำเป็นต้องใช้:
CONTAINERD_PREV=$(awk '$1 == "containerd.io" {print $2}' /root/docker-pre-upgrade.txt)
apt-get install -y --allow-downgrades docker-ce=5:28.1.1-1~debian.12~bookworm docker-ce-cli=5:28.1.1-1~debian.12~bookworm containerd.io="${CONTAINERD_PREV}" docker-compose-plugin=2.35.1-1~debian.12~bookworm
คำสั่งนี้ยังตรึง containerd.io ไว้ที่เวอร์ชันที่บันทึกในไฟล์อ้างอิงในขั้นตอนที่ 4 และส่ง --allow-downgrades เพราะ apt-get -y จะหยุดทำงานแทนที่จะลดเวอร์ชันแพ็กเกจในโหมดที่ไม่โต้ตอบกับผู้ใช้
เราเก็บคำสั่งนี้ในไฟล์ข้อความบนเซิร์ฟเวอร์ที่ /root/docker-rollback.sh พร้อมสิทธิ์เรียกใช้งาน เพื่อให้รันได้ทันทีโดยไม่ต้องพิมพ์ขณะอยู่ภายใต้ความกดดัน
ขั้นตอนอัปเกรด
เมื่อเตรียมการครบแล้ว การอัปเกรดจริงก็ตรงไปตรงมา ลำดับมีความสำคัญ: อัปเดตดัชนีแพ็กเกจก่อน ตรวจสอบสิ่งที่จะติดตั้ง แล้วจึงติดตั้ง
ขั้นตอน 1: อัปเดตดัชนี apt เฉพาะคลังแพ็กเกจ Docker
apt-get update -o Dir::Etc::sourcelist="sources.list.d/docker.list" -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"
คำสั่งนี้จำกัดการอัปเดตไว้ที่คลังแพ็กเกจ Docker แทนการรีเฟรชทุกแหล่ง บนเซิร์ฟเวอร์ใช้งานจริง การอัปเดตแพ็กเกจจากแหล่งอื่นโดยไม่ตั้งใจในช่วงบำรุงรักษา Docker เป็นตัวแปรที่ไม่จำเป็น
ขั้นตอน 2: ยืนยันเวอร์ชันที่เสนอให้ติดตั้ง
apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin
ตรวจสอบว่าเวอร์ชันที่เสนอให้ติดตั้งตรงกับที่คาดไว้ก่อนดำเนินการต่อ หากใหม่กว่าเวอร์ชันที่ทดสอบในสภาพแวดล้อมทดสอบก่อนใช้งานจริง ให้หยุดเพื่อประเมินก่อน
ขั้นตอน 3: ติดตั้ง
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
คำสั่งนี้จะหยุด Docker daemon แทนไบนารี และเริ่ม daemon ใหม่ คอนเทนเนอร์ที่ใช้ restart: always จะถูก daemon เริ่มขึ้นอีกครั้ง ช่วงที่คอนเทนเนอร์ไม่ทำงานมักสั้นกว่าสามสิบวินาทีบนเซิร์ฟเวอร์ขนาดเหมาะสม แต่ขึ้นอยู่กับระยะเวลาที่ containerd ใช้เริ่มทำงานด้วย
เฝ้าดูการเริ่มใหม่จากเทอร์มินัลอีกหน้าต่าง:
watch -n 2 'docker ps --format "table {{.Names}} {{.Status}}" | sort'
ขั้นตอน 4: ตรวจสอบเวอร์ชันที่ติดตั้ง
docker version
docker compose version
ผลลัพธ์บนเซิร์ฟเวอร์ของเรายืนยันว่า:
Docker Engine: 28.2.2
Docker Compose: v2.36.0
Docker Compose กำหนดเวอร์ชันแยกจาก Docker Engine โดยเวอร์ชันของแพ็กเกจ docker-compose-plugin (ในที่นี้คือ 2.36.0) คือเวอร์ชันของ Compose และเป็นเวอร์ชันที่ดัชนี apt ติดตาม
ตรวจสอบหลังอัปเกรด
หลังอัปเกรดสิบนาที เราตรวจสอบตามรายการอย่างเป็นระบบ ขั้นตอนนี้ข้ามไม่ได้ เพราะการเริ่ม daemon ใหม่ทำให้คอนเทนเนอร์กลับมาทำงาน แต่ไม่รับประกันว่าคอนเทนเนอร์ที่พึ่งการตรวจสอบสถานะจะผ่านการตรวจแล้ว
ตรวจสอบว่าคอนเทนเนอร์ทั้งหมดอยู่ในสถานะที่คาดไว้
docker ps -a --format "table {{.Names}} {{.Status}} {{.RunningFor}}" | sort
คอนเทนเนอร์ใดที่แสดง Restarting หรือ Exited ต้องตรวจสอบทันที บนเซิร์ฟเวอร์ของเรา คอนเทนเนอร์หนึ่งตัวซึ่งเป็นบริการวิเคราะห์ของ Supabase แสดง Restarting (1) คอนเทนเนอร์นี้วนหยุดและเริ่มใหม่เป็นครั้งคราวตั้งแต่ก่อนอัปเกรด การอัปเกรดไม่ได้ทำให้เกิดปัญหา แต่ทำให้เห็นชัดขึ้นในการตรวจหลังอัปเกรด นี่คือปัญหาเดิมประเภทที่รายการคอนเทนเนอร์ก่อนอัปเกรดมีไว้เพื่อตรวจพบ
ตรวจสอบว่าการตรวจสอบสถานะผ่าน
docker inspect $(docker ps -q) --format '{{.Name}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' | grep -v "health=none" | sort
คอนเทนเนอร์ที่ไม่ได้กำหนดการตรวจสอบสถานะไว้อย่างชัดเจนจะไม่มีสถานะสุขภาพ ขึ้นอยู่กับเวอร์ชัน Docker {{.State.Health.Status}} ที่ไม่ได้ตรวจสอบก่อนอาจแสดง <no value> หรือทำให้ทั้งคำสั่งหยุดพร้อมข้อผิดพลาดของเทมเพลต เทมเพลตจึงตรวจ .State.Health ก่อน และ grep จะตัดคอนเทนเนอร์ที่ไม่มีการตรวจสอบสถานะออก สิ่งที่ต้องสนใจคือคอนเทนเนอร์ที่ตัวอื่นพึ่งพาผ่าน condition: service_healthy
ตรวจสอบการกำหนดค่า Compose ของทุกชุดระบบ
for dir in /opt/*/; do
if [ -f "${dir}docker-compose.yml" ]; then
echo "--- $dir ---"
docker compose -f "${dir}docker-compose.yml" config --quiet 2>&1
fi
done
ขั้นตอนนี้ตรวจพบคำสั่งในไฟล์ Compose ที่ Compose เวอร์ชันใหม่มองเป็นคำเตือนหรือข้อผิดพลาด บนเซิร์ฟเวอร์ของเรา มีสามชุดระบบแสดงคำเตือนว่าคีย์ version เลิกแนะนำให้ใช้แล้ว คำเตือนเหล่านี้ไม่กระทบการทำงาน แต่ควรแก้ในรอบบำรุงรักษาถัดไป
ตรวจสอบข้อผิดพลาดในบันทึกของ Docker daemon
journalctl -u docker --since "1 hour ago" | grep -i -E "error|warn|fatal"
การอัปเกรดที่ราบรื่นจะไม่สร้างข้อผิดพลาดในบันทึกของ daemon คำเตือนเกี่ยวกับการกำหนดค่าที่เลิกแนะนำให้ใช้ควรจดไว้ แต่ไม่ต้องแก้ทันที
ยืนยันการเชื่อมต่อเครือข่ายระหว่างคอนเทนเนอร์
docker network ls
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'
คำสั่งที่สองแสดงเฉพาะเครือข่าย bridge เริ่มต้น ให้รันซ้ำกับเครือข่ายที่สร้างเองแต่ละรายการจาก docker network ls เพื่อยืนยันว่าเครือข่าย bridge ที่สร้างเองยังมีคอนเทนเนอร์เป็นสมาชิกตามที่คาดไว้หลัง daemon เริ่มใหม่ และทดสอบการเชื่อมต่อจริงอย่างน้อยหนึ่งรายการระหว่างบริการที่พึ่งพากัน
สิ่งที่เราพบ
การอัปเกรดเสร็จในเวลาไม่ถึงสองนาที คอนเทนเนอร์ทั้งหมดที่รันก่อนอัปเกรดกลับมารันหลังอัปเกรด การเปลี่ยนแปลงใน Compose ที่เราสังเกตเห็นทันทีคือรูปแบบผลลัพธ์ของ --dry-run ที่ชัดเจนขึ้น เวอร์ชันใหม่แสดงผลแบบมีโครงสร้างและใช้สี ทำให้ตรวจได้ง่ายขึ้นว่า docker compose up จะทำอะไรจริงก่อนเรียกใช้
การจัดลำดับการตรวจสอบสถานะด้วย depends_on ทำงานเหมือน Compose 2.35 ในชุดระบบของเรา เป็นผลที่คาดไว้เพราะตรวจสอบไฟล์ Compose ล่วงหน้าแล้ว แต่การยืนยันก็ช่วยให้มั่นใจขึ้น
ข้อค้นพบสำคัญข้อหนึ่งคือ Compose ไม่อนุญาตให้ใช้ฟิลด์ container_name ร่วมกับ scale เพราะชื่อคอนเทนเนอร์ต้องไม่ซ้ำกัน เรามีชุดระบบหนึ่งที่ใช้ทั้งสองอย่าง ซึ่งค้างมาจากก่อนเปลี่ยนไปใช้ replica อย่างถูกต้อง Compose แสดงข้อผิดพลาดอย่างชัดเจนเมื่อรัน docker compose up กับชุดระบบนั้น วิธีแก้คือแก้เพียงหนึ่งบรรทัดเพื่อลบฟิลด์ container_name ที่ไม่จำเป็นออก