tva
← บทความเชิงลึก

อัปเกรด 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 ที่ไม่จำเป็นออก

บทความที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง