วิธีตั้งค่า Docker Compose ให้เริ่มทำงานอัตโนมัติหลังรีบูต
ตั้งค่า Docker Compose ให้รันอัตโนมัติหลังบูตเครื่องด้วย restart policy แบบ always หรือ unless-stopped พร้อมวิธีตรวจสอบสถานะผ่าน docker inspect และการใช้ systemd unit
คำตอบโดยย่อ
บริการ Docker Compose จะเริ่มทำงานเมื่อบูตเครื่องเมื่อเงื่อนไขสองประการเกิดขึ้นพร้อมกัน ประการแรก Docker daemon ต้องถูกเปิดใช้งานเป็น system service และประการที่สอง แต่ละบริการในไฟล์ต้องมีนโยบายการรีสตาร์ทเป็น unless-stopped หรือ always ให้เพิ่ม restart: unless-stopped ลงในทุกบริการและรันคำสั่ง docker compose up -d หนึ่งครั้ง คอนเทนเนอร์จะกลับมาทำงานเองโดยอัตโนมัติหลังจากรีบูตเครื่อง สำหรับกรณีทั่วไปไม่จำเป็นต้องตั้งค่าอื่นเพิ่มเติม
คุณจำเป็นต้องใช้ systemd unit ก็ต่อเมื่อลำดับการทำงานมีความสำคัญ เช่น stack ที่ต้องพึ่งพา disk ที่ mount ไว้, VPN interface หรือ network share ที่ยังไม่พร้อมใช้งานในขณะที่ Docker daemon เริ่มทำงาน กรณีนี้เป็นปัญหาที่เกิดขึ้นจริง และเนื้อหาในส่วนที่สองของคู่มือนี้จะครอบคลุมวิธีจัดการ หากคุณยังอยู่ในช่วงเริ่มต้นทำความเข้าใจเกี่ยวกับการกำหนดค่าบริการและ volume ให้เริ่มจาก พื้นฐานของ Docker Compose บน VPS แล้วค่อยกลับมาอ่านส่วนนี้อีกครั้ง
กำหนดนโยบายการรีสตาร์ทใน compose.yaml
นโยบายนี้กำหนดได้หนึ่งบรรทัดต่อหนึ่งบริการ ไม่มีสวิตช์ส่วนกลาง ดังนั้นบริการที่คุณลืมกำหนดค่าจะหยุดทำงานหลังรีบูต ในขณะที่บริการอื่นใน stack จะเริ่มทำงานตามปกติ
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:ใช้การตั้งค่านี้แล้วตรวจสอบนโยบายจากคอนเทนเนอร์ที่กำลังทำงานอยู่:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)คำสั่งนี้จะแสดงผลเป็น unless-stopped หากแสดงผลเป็น no แสดงว่ามีการแก้ไขไฟล์แล้วแต่คอนเทนเนอร์ยังไม่ได้ถูกสร้างใหม่
นี่คือสาเหตุความล้มเหลวที่พบบ่อยที่สุด นโยบายการรีสตาร์ทจะถูกเก็บไว้ที่ตัว container ไม่ใช่ในไฟล์ YAML การแก้ไข compose.yaml จะไม่มีผลใดๆ กับคอนเทนเนอร์ที่มีอยู่เดิม ส่วน docker compose restart ก็ไม่ช่วยเช่นกัน เพราะเป็นการหยุดและเริ่มคอนเทนเนอร์ตัวเดิมโดยไม่ได้แตะต้องค่าคอนฟิกูเรชัน มีเพียง docker compose up -d เท่านั้นที่จะเปรียบเทียบไฟล์กับคอนเทนเนอร์ที่ทำงานอยู่ ตรวจพบว่านโยบายมีการเปลี่ยนแปลง และทำการสร้างคอนเทนเนอร์ใหม่ให้
สำหรับคอนเทนเนอร์ที่คุณยังไม่ต้องการสร้างใหม่ในขณะนี้ ให้เปลี่ยนนโยบายโดยตรงที่ตัวคอนเทนเนอร์:
docker update --restart unless-stopped my-containerอย่างไรก็ตาม คุณยังต้องแก้ไขไฟล์ YAML ควบคู่ไปด้วย เพราะ docker update จะเปลี่ยนค่าในคอนเทนเนอร์ที่ทำงานอยู่เท่านั้น และการใช้ docker compose up -d ในครั้งถัดไปจะอ่านค่าจากไฟล์และนำค่านโยบายเดิมกลับมาเขียนทับอีกครั้ง
การทำงานของค่า restart แต่ละแบบ
Docker กำหนดค่าไว้ 4 รูปแบบ ซึ่งความแตกต่างจะปรากฏให้เห็นเมื่อเครื่องรีบูตหรือเมื่อ Docker daemon เริ่มทำงานใหม่เท่านั้น
noเป็นค่าเริ่มต้น คอนเทนเนอร์จะไม่ถูกรีสตาร์ทโดยอัตโนมัติไม่ว่าในกรณีใดก็ตามalwaysจะรีสตาร์ทคอนเทนเนอร์ทุกครั้งที่หยุดทำงาน หากคุณหยุดคอนเทนเนอร์ด้วยตนเอง มันจะกลับมาทำงานอีกครั้งเมื่อ Docker daemon เริ่มทำงานใหม่ ซึ่งมักจะสร้างความประหลาดใจได้ เพราะคอนเทนเนอร์ที่คุณตั้งใจหยุดไปเมื่อสัปดาห์ก่อนจะกลับมาทำงานอีกครั้งหลังรีบูตunless-stoppedมีการทำงานคล้ายกับalwaysแต่มีข้อแตกต่างคือ หากคอนเทนเนอร์ถูกหยุดด้วยตนเอง มันจะยังคงหยุดทำงานอยู่แม้จะมีการรีสตาร์ท daemon นี่คือค่าที่เหมาะสมสำหรับบริการที่คุณต้องการหยุดเพื่อบำรุงรักษาเป็นครั้งคราวon-failureจะรีสตาร์ทคอนเทนเนอร์เฉพาะเมื่อมีการออกจากโปรแกรม (exit) ด้วย exit code ที่ไม่ใช่ 0 เท่านั้น คุณสามารถจำกัดจำนวนครั้งที่พยายามรีสตาร์ทได้ ดังเช่นในrestart: on-failure:3
สำหรับ stack ที่ควรจะทำงานอยู่เสมอเมื่อเซิร์ฟเวอร์เปิดใช้งาน unless-stopped คือค่าเริ่มต้นที่เหมาะสมที่สุด ให้เลือกใช้ always เฉพาะในกรณีที่คุณต้องการคอนเทนเนอร์ที่ไม่ยอมหยุดทำงานแม้จะถูกสั่งปิดไปแล้วก็ตาม
เหตุใด restart: on-failure จึงไม่ทำงานหลังการรีบูต
หลายคนเลือกใช้ on-failure เพราะฟังดูปลอดภัย แต่กลับพบว่าคอนเทนเนอร์ทุกตัวหยุดทำงานหลังจากรีบูตครั้งแรก เหตุผลอยู่ในนิยามของมัน on-failure ตอบสนองต่อสิ่งเดียวเท่านั้น คือการที่กระบวนการของคอนเทนเนอร์จบการทำงานด้วยรหัสข้อผิดพลาด (error code)
การรีบูตไม่ใช่ข้อผิดพลาด เมื่อโฮสต์ปิดตัวลง systemd จะหยุด docker.service และ daemon จะหยุดคอนเทนเนอร์แต่ละตัวอย่างตั้งใจ คอนเทนเนอร์ไม่ได้ล้มเหลว นโยบายนี้จึงไม่มีเหตุให้ต้องตอบสนอง ในระหว่างที่ระบบเริ่มทำงานใหม่ daemon จะตรวจสอบคอนเทนเนอร์ที่จำเป็นต้องกลับมาทำงานต่อ แต่คอนเทนเนอร์แบบ on-failure ที่ถูกหยุดอย่างเป็นระเบียบไม่ใช่หนึ่งในนั้น มันจึงคงสถานะ exited เอาไว้
คุณสามารถตรวจสอบได้โดยตรง กำหนดค่า restart: on-failure ให้กับ service รันคำสั่ง docker compose up -d จากนั้นรีบูต แล้วรันคำสั่ง:
docker compose ps -aservice จะแสดงสถานะเป็น Exited และมีสถานะย่อยเป็น Exited (0) 2 minutes ago ไม่มีสิ่งใดเสียหายและไม่มีการบันทึกข้อผิดพลาดใน log ซึ่งเป็นสาเหตุที่ทำให้การวินิจฉัยปัญหานี้ทำได้ยาก นโยบายดังกล่าวทำงานตรงตามที่ระบุไว้ทุกประการ
on-failure ยังคงมีประโยชน์ มันเหมาะสำหรับคอนเทนเนอร์ที่รันงานเฉพาะอย่างและอาจเกิดการ crash ซึ่งคุณต้องการจำกัดจำนวนครั้งในการลองใหม่และไม่ต้องการให้เกิดลูปการรีสตาร์ท แต่มันเป็นเครื่องมือที่ไม่เหมาะสมสำหรับการรักษา service ที่รันต่อเนื่องให้ทำงานอยู่เสมอหลังการรีบูต
นโยบายการรีสตาร์ทจะทำงานก็ต่อเมื่อ Docker service เริ่มทำงานตอนบูตเครื่องเท่านั้น
นโยบายการรีสตาร์ทถูกบังคับใช้โดย Docker daemon หาก daemon ไม่เริ่มทำงาน ก็จะไม่มีสิ่งใดบังคับใช้นโยบายเหล่านี้ได้ ให้ตรวจสอบดังนี้:
systemctl is-enabled docker
systemctl is-enabled containerdทั้งสองคำสั่งควรแสดงผลเป็น enabled แพ็กเกจจาก repository อย่างเป็นทางการของ Docker จะเปิดใช้งานสิ่งเหล่านี้ไว้ตั้งแต่ตอนติดตั้ง ดังนั้นบนเซิร์ฟเวอร์ที่ติดตั้งใหม่มักจะผ่านการตรวจสอบนี้ หากคำสั่งใดแสดงผลเป็น disabled ให้แก้ไขดังนี้:
sudo systemctl enable --now docker containerdมีกับดักที่ควรทำความเข้าใจอยู่จุดหนึ่ง Ubuntu มี docker.socket มาให้ด้วย ซึ่งจะเริ่ม daemon ตามความต้องการในครั้งแรกที่มีการเรียกใช้งาน Docker API ผู้ใช้งานมักเห็นว่า docker.socket ถูกเปิดใช้งานอยู่ จึงเข้าใจไปว่า daemon ได้รับการจัดการเรียบร้อยแล้ว และทำการปิด docker.service เพื่อประหยัดหน่วยความจำ ในขณะบูตเครื่องจะไม่มีการเรียกใช้งาน API ดังนั้น socket จึงไม่ถูกเรียกใช้ daemon จึงไม่เริ่มทำงาน และจะไม่มี container ใดขึ้นมาจนกว่าคุณจะพิมพ์คำสั่ง docker ครั้งแรก การทำ socket activation ไม่สามารถใช้แทนการเปิดใช้งาน docker.service ได้
เมื่อ systemd unit เป็นทางเลือกที่ดีกว่า
นโยบายการ restart ไม่มีกลไกในการจัดการลำดับการทำงานเมื่อเทียบกับส่วนอื่นของระบบ เมื่อ daemon เริ่มทำงาน มันจะพยายามนำ container ของคุณขึ้นมาทันทีที่ทำได้ หาก stack ของคุณมีการ bind-mount ไดเรกทอรีจาก volume แยก, NFS (network file system) share หรือดิสก์ที่เข้ารหัสไว้ container อาจเริ่มทำงานก่อนที่ path ดังกล่าวจะพร้อมใช้งาน Docker จะสร้างไดเรกทอรีว่างที่จุด mount นั้นและเริ่ม container โดยอัตโนมัติ ส่งผลให้ฐานข้อมูลของคุณเริ่มทำงานโดยไม่มีข้อมูล
ให้เขียน systemd unit เมื่อเข้าเงื่อนไขใดเงื่อนไขหนึ่งต่อไปนี้: stack ต้องการ mount, VPN interface หรือ unit อื่นให้พร้อมใช้งานก่อน คุณต้องการให้ systemctl stop myapp และ systemctl start myapp ทำงานเหมือนกับ service อื่นๆ บนเครื่อง หรือคุณต้องการให้ stack ถูกปิดอย่างถูกต้องในระหว่างการ shutdown แทนที่จะถูก kill ไปพร้อมกับ daemon หากคุณยังไม่คุ้นเคยกับ systemd unit เนื้อหาใน การเขียน systemd service และ timer จะอธิบายรูปแบบไฟล์ไว้อย่างละเอียด
การเขียน systemd unit
ให้วาง stack ไว้ใน path ที่กำหนดไว้นอก home directory โดย /srv/myapp เป็นตัวเลือกที่ดี เพราะ unit ที่ทำงานก่อนการล็อกอินของผู้ใช้ไม่มีความจำเป็นต้องเข้าถึง /home
สร้างไฟล์ /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.targetเปิดใช้งานและเริ่มการทำงาน:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceunit ที่ทำงานปกติจะแสดงสถานะ Active: active (exited) ซึ่งอาจดูไม่ถูกต้องในครั้งแรกที่เห็น แต่ความจริงคือถูกต้องแล้ว: Type=oneshot พร้อมกับ RemainAfterExit=yes หมายความว่า unit ได้รันคำสั่งเรียบร้อยแล้ว คำสั่งทำงานเสร็จสิ้น และ systemd ยังคงทำเครื่องหมาย unit ว่า active เพื่อให้ ExecStop ทำงานในขั้นตอนการปิดระบบ
ทุกบรรทัดมีความสำคัญ Requires=docker.service หมายความว่า unit จะล้มเหลวทันทีแทนที่จะพยายามรัน docker compose กับ socket ที่ไม่ทำงาน After= กำหนดลำดับการทำงาน เนื่องจาก Requires= เพียงอย่างเดียวไม่สามารถทำได้ RequiresMountsFor= ทำให้ systemd ดึง mount unit สำหรับ path นั้นเข้ามาและรอจนกว่าจะพร้อม ซึ่งเป็นเหตุผลหลักในการใช้ unit แทนการพึ่งพา restart policy เพียงอย่างเดียว TimeoutStartSec=0 ป้องกันไม่ให้ systemd สั่งหยุด start job ในขณะที่กำลังดึง image ขนาดใหญ่
ข้อควรทราบเกี่ยวกับการใช้ทั้งสองกลไกควบคู่กัน เอกสารของ Docker แนะนำไม่ให้ใช้ restart policy ร่วมกับ process manager ของ host คำเตือนดังกล่าวหมายถึง process manager ที่คอยควบคุมกระบวนการของ container โดยตรงและพยายาม restart ในขณะที่ daemon ก็พยายามทำเช่นเดียวกัน แต่ unit แบบ Type=oneshot ไม่ได้ควบคุมสิ่งใด ดังนั้นการคง restart: unless-stopped ไว้ในไฟล์ compose ควบคู่กับ unit นี้จึงสามารถทำได้และเป็นสิ่งที่ควรทำ โดย systemd จะจัดการลำดับการทำงานตอนบูต และ daemon จะจัดการ container ที่เกิด crash ในช่วงเวลาอื่น
unit จะมีลักษณะแตกต่างออกไปเมื่อสิ่งที่คุณต้องการให้ทำงานต่อเนื่องเป็น process ปกติที่รันยาวนานแทนที่จะเป็น stack เนื่องจากไม่มี daemon คอยควบคุมอยู่เบื้องหลัง และ Restart= ของ systemd เองจะต้องทำหน้าที่ควบคุมแทน การรัน dsh แบบ headless ผ่าน systemd เป็นตัวอย่างการใช้งานในรูปแบบดังกล่าว ซึ่งรวมถึงการกำหนดผู้ใช้เฉพาะและการจัดการผ่าน journal
ตรวจสอบด้วยการรีบูตจริง
ไม่มีสิ่งใดทดแทนการทดสอบจริงได้ systemctl restart docker ไม่ได้ทดสอบลำดับการ mount และ docker compose down ตามด้วย docker compose up -d ไม่ได้ทดสอบกระบวนการบูตแต่อย่างใด
sudo rebootรอให้ระบบกลับมา เชื่อมต่อใหม่ และตรวจสอบตามลำดับดังนี้:
uptime
systemctl is-active docker
docker compose psuptime ยืนยันว่าคุณกำลังตรวจสอบเครื่องที่รีบูตจริง docker compose ps ซึ่งรันจากไดเรกทอรีของ stack ควรแสดงรายการทุกบริการเป็น running โดยมีค่า uptime ใกล้เคียงกับของเครื่อง บริการที่แสดงสถานะ Exited คือจุดที่ต้องตรวจสอบ
หากมีบางอย่างไม่ทำงาน ให้ดู log ของ daemon ในช่วงเวลาที่บูต:
journalctl -u docker.service -b --no-pager | tail -50สำหรับ stack ที่จัดการด้วย unit นั้น journalctl -u myapp.service -b --no-pager จะแสดงผลลัพธ์ docker compose ที่เกิดขึ้นจริงตั้งแต่ตอนบูต รวมถึงกรณีที่ดึง image ล้มเหลวหรือไฟล์ .env หายไป การรีบูตที่คุณกำหนดเวลาไว้คือสิ่งที่คุณต้องเฝ้าดู ดังนั้นให้ unit แจ้งเตือนคุณเกี่ยวกับสิ่งที่ไม่ได้เป็นไปตามแผน โดยการใช้บรรทัด OnFailure= ชี้ไปยัง เซิร์ฟเวอร์ ntfy ที่โฮสต์เอง จะเปลี่ยน stack ที่ไม่สามารถกลับมาทำงานได้ให้กลายเป็นการแจ้งเตือนแบบ push แทนที่จะเป็นปัญหาที่คุณเพิ่งมาพบในอีกหลายวันให้หลัง
สาเหตุที่ทำให้การเริ่มทำงานอัตโนมัติล้มเหลวโดยไม่แจ้งเตือน
คอนเทนเนอร์ที่สร้างด้วย docker compose run จะไม่ได้รับนโยบายการรีสตาร์ทจากไฟล์ Compose จะมองว่าคอนเทนเนอร์เหล่านั้นเป็นแบบใช้ครั้งเดียว หากบริการดูเหมือนจะเพิกเฉยต่อนโยบายของคุณ ให้ตรวจสอบว่าบริการนั้นถูกเริ่มด้วย run แทนที่จะเป็น up หรือไม่
พาธแบบสัมพัทธ์ (relative path) ใน volume หรือในรายการ env_file จะถูกอ้างอิงจากไดเรกทอรีของไฟล์ compose ซึ่งทำงานได้ปกติเมื่อรันจาก shell และทำงานได้จาก unit ที่ตั้งค่า WorkingDirectory ไว้ แต่จะล้มเหลวหาก unit นั้นไม่มีการตั้งค่าดังกล่าว เพราะไดเรกทอรีทำงาน (working directory) จะกลายเป็น / แทน
Rootless Docker เป็นกรณีเฉพาะ เนื่องจาก daemon ทำงานในฐานะ user service และ user service จะหยุดทำงานเมื่อ session สุดท้ายของผู้ใช้นั้นสิ้นสุดลง ให้เปิดใช้งานสำหรับผู้ใช้และอนุญาตให้ทำงานต่อไปได้แม้ไม่มีใครล็อกอินอยู่:
systemctl --user enable docker
sudo loginctl enable-linger $USERหากไม่มี enable-linger ตัว rootless daemon จะปิดตัวลงเมื่อคุณล็อกเอาต์และคอนเทนเนอร์ทั้งหมดจะหยุดทำงานตามไปด้วย ซึ่งดูเหมือนกับนโยบายการรีสตาร์ทมีปัญหา
สิ่งสุดท้ายคือ การอัปเดตความปลอดภัยอัตโนมัติอาจรีบูตเซิร์ฟเวอร์ตามเวลาที่กำหนด ซึ่งจะเป็นเรื่องดีก็ต่อเมื่อ stack ของคุณสามารถกลับมาทำงานได้เอง การตั้งค่าส่วนนี้ควรทำพร้อมกับงานอื่นๆ ในชั่วโมงแรกของการติดตั้งใน สิบนาทีแรกบน VPS เครื่องใหม่
FAQ
restart: always กับ restart: unless-stopped ต่างกันอย่างไร
ทั้งสองคำสั่งจะเริ่ม container ใหม่เมื่อหยุดทำงานเองโดยอัตโนมัติ แต่จะมีความแตกต่างเมื่อคุณหยุด container ด้วยตนเอง หากใช้ always ตัว container จะเริ่มทำงานใหม่ในครั้งถัดไปที่ Docker daemon เริ่มทำงาน ดังนั้นการรีบูตเครื่องจะทำให้สถานะการหยุดด้วยตนเองของคุณถูกยกเลิก แต่หากใช้ unless-stopped ตัว daemon จะจดจำว่า container ถูกหยุดโดยเจตนาและจะไม่ไปยุ่งกับมัน ให้ใช้ unless-stopped เว้นแต่ว่าคุณต้องการให้ container นั้นทำงานตลอดเวลาโดยไม่ยอมหยุด
ฉันเพิ่ม restart: unless-stopped แล้ว แต่ container ยังไม่เริ่มทำงานหลังรีบูต เพราะอะไร
นโยบายนี้จะผูกอยู่กับตัว container ไม่ใช่ที่ไฟล์ config และ container ที่สร้างไปแล้วจะไม่ถูกอัปเดตเพียงแค่การแก้ไขไฟล์ YAML ให้รันคำสั่ง docker compose up -d เพื่อให้ Compose สร้าง container ขึ้นมาใหม่ จากนั้นตรวจสอบด้วย docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app) หากผลลัพธ์แสดง no แสดงว่า container นั้นถูกสร้างขึ้นก่อนที่คุณจะแก้ไขไฟล์ สาเหตุทั่วไปอีกประการหนึ่งคือ docker.service ไม่ได้ถูกเปิดใช้งาน ซึ่งคุณสามารถตรวจสอบได้ด้วย systemctl is-enabled docker
ฉันจำเป็นต้องใช้ systemd unit หรือไม่หากใช้นโยบาย restart อยู่แล้ว
โดยปกติแล้วไม่จำเป็น นโยบาย restart เพียงพอสำหรับ stack ที่ต้องการเพียงแค่เครือข่าย ซึ่งเป็นกรณีส่วนใหญ่ ให้เพิ่ม unit เข้าไปก็ต่อเมื่อ container มีความจำเป็นต้องพึ่งพาสิ่งที่ยังไม่พร้อมในขณะที่ Docker daemon เริ่มทำงาน เช่น ดิสก์ภายนอก, encrypted volume, NFS share หรือ VPN interface ตัว unit จะช่วยให้คุณกำหนดลำดับการทำงานผ่าน After= และ RequiresMountsFor= ซึ่งนโยบาย restart ไม่สามารถทำได้
ฉันจะหยุด stack อย่างถาวรไม่ให้กลับมาทำงานใหม่หลังรีบูตได้อย่างไร
หากใช้ unless-stopped การใช้คำสั่ง docker compose stop ก็เพียงพอแล้ว เพราะ container ที่ถูกหยุดด้วยตนเองจะไม่ถูกเริ่มใหม่เมื่อ daemon เริ่มทำงาน แต่หากใช้ always การหยุดเพียงอย่างเดียวไม่เพียงพอและ container จะกลับมาทำงานใหม่หลังรีบูต ให้รัน docker compose down เพื่อลบ container ทิ้ง หรือเปลี่ยนนโยบายก่อนด้วย docker update --restart no my-container หากมี systemd unit คอยจัดการ stack อยู่ ให้รัน sudo systemctl disable myapp.service ด้วย มิฉะนั้น unit จะสั่งให้มันเริ่มทำงานใหม่อีกครั้ง