วิธีติดตั้ง Docker Compose บน Ubuntu 24.04 สำหรับ VPS
เรียนรู้วิธีติดตั้ง Docker Engine และปลั๊กอิน Compose บน Ubuntu 24.04 พร้อมตัวอย่างไฟล์ compose.yml สองบริการ วิธีแก้ปัญหาพอร์ตทะลุผ่าน ufw และการสำรองข้อมูล volume อย่างถูกต้อง
สิ่งที่คุณกำลังสร้าง
Docker Compose คือโครงสร้างพื้นฐานที่อยู่เบื้องหลังเกือบทุกอย่างบนเว็บไซต์นี้ ไม่ว่าจะเป็น Nextcloud, Vaultwarden, n8n, Immich, หรือ Rocket.Chat คู่มือเหล่านี้ล้วนเริ่มต้นด้วยการ "เขียนไฟล์ compose" และหน้านี้จะอธิบายความหมายที่แท้จริงของไฟล์ดังกล่าว คุณจะได้ติดตั้ง Docker Engine และปลั๊กอิน Compose จาก apt repository ของ Docker โดยตรงบน Ubuntu 24.04 จากนั้นจึงสร้าง stack สองบริการจริงขึ้นมา ได้แก่ Miniflux ซึ่งเป็นโปรแกรมอ่าน RSS ขนาดเล็ก และ PostgreSQL เนื่องจากคู่บริการนี้ช่วยให้คุณได้ฝึกฝนทุกรูปแบบที่แอปขนาดใหญ่ใช้งาน ไม่ว่าจะเป็นการระบุเวอร์ชัน image (pinned images), ฐานข้อมูลที่มีการตรวจสอบสถานะ (healthcheck), การใช้ named volume, การเก็บข้อมูลลับในไฟล์ .env และการเปิดพอร์ตเฉพาะ localhost เท่านั้น
การติดตั้งใช้เวลาเพียง 5 นาที ส่วนที่เหลือของคู่มือนี้จะครอบคลุมประเด็นที่อาจสร้างปัญหาในภายหลัง เช่น การที่กลุ่ม docker มีสิทธิ์เทียบเท่า root, การที่พอร์ตที่เปิดไว้สามารถทะลุผ่านกฎ ufw ของคุณได้โดยตรง และ flag ตัวหนึ่งใน docker compose down ที่จะลบฐานข้อมูลของคุณทิ้งโดยไม่มีการแจ้งเตือนยืนยัน
ข้อกำหนดเบื้องต้น: VPS ประเภท KVM ที่ใช้ Ubuntu 24.04 ใหม่ล่าสุด, ผู้ใช้ที่มีสิทธิ์ sudo และมี RAM ขนาด 1 กิกะไบต์ขึ้นไป หากคุณมีการติดตั้ง Docker อยู่แล้วก็สามารถใช้งานได้เช่นกัน โดยส่วนแรกของคู่มือจะอธิบายถึงสิ่งที่ควรลบออกก่อน
ติดตั้งจาก repository ของ Docker ไม่ใช่ของ Ubuntu
มีสองสิ่งที่ควรหลีกเลี่ยงก่อนเริ่มคำสั่งแรก แพ็กเกจ docker.io ของ Ubuntu เองนั้นใช้งานได้ แต่เวอร์ชันมักจะล้าหลังกว่าของ Docker และไม่มีโครงสร้าง plugin ที่ซอฟต์แวร์อื่นคาดหวัง ส่วนคำสั่ง docker-compose ที่มีขีดกลางเป็นสิ่งที่บทความเก่าๆ มักแนะนำ บนเซิร์ฟเวอร์รุ่นเก่าคำสั่งนี้มักหมายถึง Compose v1 ซึ่งเขียนด้วย Python และ Docker ได้ประกาศยุติการสนับสนุนไปแล้ว จึงเป็นสาเหตุที่ทำให้บทความเหล่านั้นใช้งานไม่ได้ ปัจจุบัน Docker ยังคงเผยแพร่ไฟล์ binary docker-compose แบบ standalone ที่เขียนด้วย Go แต่หน้าเว็บติดตั้งของ Docker เองระบุว่าวิธีนี้ไม่แนะนำและรองรับไว้เพื่อความเข้ากันได้กับระบบเก่าเท่านั้น ปัจจุบัน Compose คือ docker compose (แบบเว้นวรรค) ซึ่งเป็น CLI plugin ที่ติดตั้งจาก repository เดียวกับตัว engine
หากมีซอฟต์แวร์เหล่านี้อยู่ในเครื่อง ให้ลบออกก่อน รวมถึง docker-compose-v2 และ docker-buildx ซึ่งเป็นแพ็กเกจ plugin ของ Ubuntu เพื่อให้ทุกอย่างมาจาก repository เดียวกัน:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runcPackage 'docker.io' is not installed, so not removed คือผลลัพธ์ปกติบน VPS ใหม่ จากนั้นให้เพิ่ม repository ของ Docker และติดตั้ง:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginตรวจสอบทั้งสามส่วน:
docker --version
docker compose version
sudo docker run --rm hello-worldสองคำสั่งแรกจะแสดงเวอร์ชัน ส่วน Docker Compose version v5.x.x ยืนยันว่าคุณมี plugin ไม่ใช่ binary v1 ที่เลิกใช้แล้ว ตัว plugin ได้ข้ามจาก v2 ไปเป็น v5 ในเดือนธันวาคม 2025 เพื่อป้องกันความสับสนกับเวอร์ชันของไฟล์ compose ดังนั้นคู่มือที่แสดง v2.x.x จึงหมายถึงเครื่องมือตัวเดียวกัน การรัน hello-world ควรจบด้วย Hello from Docker! แพ็กเกจจะเปิดการทำงานของ service อัตโนมัติเมื่อบูตเครื่อง และ systemctl is-enabled docker จะแสดงผลเป็น enabled
กลุ่ม docker มีสิทธิ์เทียบเท่า root โปรดตัดสินใจด้วยความเข้าใจ
ในขณะนี้ ทุกคำสั่ง docker จำเป็นต้องใช้ sudo เนื่องจาก socket ของ daemon ที่ /var/run/docker.sock ถูกกำหนดความเป็นเจ้าของไว้ที่ root และกลุ่ม docker หากคุณไม่ได้เป็นสมาชิกของกลุ่มนี้ คุณจะพบกับข้อผิดพลาดของ Docker ที่ถูกค้นหามากที่สุด:
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sockวิธีแก้ไขมาตรฐานคือ:
sudo usermod -aG docker $USERการเป็นสมาชิกกลุ่มจะมีผลเมื่อเข้าสู่ระบบใหม่ ดังนั้นข้อผิดพลาดจะยังคงอยู่หากคุณใช้ shell เดิม ให้รัน newgrp docker สำหรับ session นี้ หรือออกจากระบบแล้วเข้าใหม่ จากนั้น id ควรแสดง docker ในรายการกลุ่มของคุณ
ตอนนี้คือส่วนที่ต้องพูดตามตรง: การเป็นสมาชิกกลุ่ม docker มีสิทธิ์เทียบเท่า root บนโฮสต์ ไม่ใช่ "เกือบเป็น root" ไม่ใช่ "สิทธิ์ที่สูงขึ้น" แต่คือ root จริงๆ ใครก็ตามที่อยู่ในกลุ่มนี้สามารถรัน docker run --rm -it -v /:/host alpine chroot /host และเข้าถึงระบบไฟล์ทั้งหมดได้โดยไม่ต้องใช้รหัสผ่าน กลุ่มนี้มีไว้เพื่อความสะดวก ไม่ใช่เพื่อการจำกัดสิทธิ์
โหมด rootless ของ Docker คือทางเลือกที่แท้จริง โดย daemon จะรันในฐานะผู้ใช้ที่ไม่มีสิทธิ์พิเศษของคุณ แต่คุณต้องแลกกับบางอย่าง: พอร์ตที่ต่ำกว่า 1024 ต้องมีการตั้งค่าเพิ่มเติม, ระบบเครือข่ายจะทำงานผ่าน userspace shim ซึ่งมี overhead ที่วัดผลได้ และ image บางตัวอาจทำงานไม่ถูกต้องหากไม่มีสิทธิ์ root จริง บน VPS ที่มีผู้ดูแลระบบคนเดียวและมีสิทธิ์ sudo อยู่แล้ว การเปลี่ยนกลุ่มนี้ในทางปฏิบัติไม่ได้เปลี่ยนแปลงระดับความปลอดภัย และเป็นสิ่งที่คู่มือทุกฉบับในที่นี้ใช้เป็นสมมติฐาน เพียงแต่อย่ามอบสิทธิ์นี้ให้ใครโดยคิดว่ามันมีค่าน้อยกว่า sudo
โครงสร้างของไฟล์ compose
ให้แต่ละ stack มีไดเรกทอรีของตัวเอง โดยชื่อไดเรกทอรีจะกลายเป็นชื่อโปรเจกต์ ซึ่งจะถูกนำไปใช้เป็นคำนำหน้าชื่อคอนเทนเนอร์, เน็ตเวิร์ก และโวลุ่ม:
sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/minifluxให้สร้าง compose.yml (เป็นชื่อเรียกสมัยใหม่ ส่วน docker-compose.yml ยังคงใช้งานได้) ให้ข้ามคีย์ version: แบบเก่าไป เพราะมันล้าสมัยแล้วและ Compose จะแจ้งเตือนหากพบการใช้งาน
services:
miniflux:
image: miniflux/miniflux:2.2.9
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=miniflux
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:ทุกบรรทัดข้างต้นคือการตัดสินใจ ให้พิจารณาทีละส่วน
การล็อกเวอร์ชันของอิมเมจ, :latest และการ pull คือการอัปเกรดโดยไม่มีการควบคุม
ใช้ postgres:16-alpine ไม่ใช่ postgres:latest เพราะแท็กไม่ใช่ค่าที่คงที่ :latest จะถูกแก้ไขค่าใหม่เป็นเวอร์ชันล่าสุดที่ผู้ดูแลอิมเมจเพิ่ง push ขึ้นไปทุกครั้งที่คุณสั่ง pull เมื่อรวมกับนิสัยการอัปเกรดตามปกติที่คุณกำลังจะเรียนรู้คือ docker compose pull && docker compose up -d จะทำให้ :latest หมายความว่าการอัปเกรดเวอร์ชันหลักจะเกิดขึ้นทันทีที่ต้นทางปล่อยออกมา ไม่ใช่เมื่อคุณเลือกเอง สำหรับ PostgreSQL นี่ไม่ใช่เรื่องสมมติ: การกระโดดข้ามเวอร์ชันจาก 16 ไป 17 โดยไม่คาดคิดจะทำให้คอนเทนเนอร์ติดอยู่ในลูปการ crash เนื่องจากข้อมูลไม่เข้ากัน เพราะการอัปเกรดเวอร์ชันหลักของ Postgres ต้องใช้วิธี dump และ restore ไม่ใช่แค่การ restart
อย่างน้อยควรล็อกเวอร์ชันหลักไว้ (postgres:16-alpine จะตามด้วยการอัปเดตแพตช์ 16.x) และล็อกแอปพลิเคชันไว้ที่รีลีสที่แน่นอน เช่น miniflux/miniflux:2.2.9 โดยตรวจสอบหน้ารีลีสของโปรเจกต์และใช้เวอร์ชันที่เป็นปัจจุบันในขณะที่คุณเขียนไฟล์ การอัปเกรดจะกลายเป็นการแก้ไขเพียงบรรทัดเดียวที่คุณตั้งใจทำ ซึ่งสามารถตรวจสอบได้ใน git diff
การ publish ไปที่ 127.0.0.1 เพราะ Docker ข้ามการทำงานของ ufw
"127.0.0.1:8080:8080" ประกอบด้วยที่อยู่โฮสต์, พอร์ตโฮสต์ และพอร์ตคอนเทนเนอร์ บทช่วยสอนส่วนใหญ่มักเขียน "8080:8080" ซึ่งเป็นรูปแบบย่อของ 0.0.0.0:8080:8080 คือการฟังทุกอินเทอร์เฟซ รวมถึงอินเทอร์เฟซสาธารณะด้วย
นี่คือกับดักที่เกือบทุกคนต้องเจอ Docker จะ publish พอร์ตโดยการเขียนกฎ DNAT เพื่อเปลี่ยนปลายทางของแพ็กเก็ตไปยัง IP ภายในของคอนเทนเนอร์ ก่อน ที่จะผ่านการกรอง ดังนั้นแพ็กเก็ตจะใช้เส้นทาง FORWARD และไม่ผ่าน INPUT ซึ่งเป็นที่ที่กฎ ufw ของคุณทำงานอยู่ sudo ufw deny 8080 จะรายงานว่าสำเร็จ ufw status จะแสดงว่าพอร์ตถูกปฏิเสธ แต่บริการยังคงตอบสนองต่ออินเทอร์เน็ตทั้งหมด ไฟร์วอลล์ของคุณไม่ได้เสีย แต่ถูกข้ามโดยการออกแบบ ทำไม Docker ถึงข้าม ufw และวิธีกรองทราฟฟิกของคอนเทนเนอร์อย่างแท้จริง จะอธิบายกลไกนี้และวิธีแก้ไขด้วย DOCKER-USER สำหรับพอร์ตที่จำเป็นต้องเปิดเป็นสาธารณะ
นิสัยที่จะทำให้ปัญหานี้หมดไปคือ: ผูกพอร์ตที่ publish ไว้กับ 127.0.0.1 เว้นแต่คุณจะมีเหตุผลเฉพาะเจาะจงที่จะไม่ทำ และใช้ reverse proxy ไว้ด้านหน้าสำหรับทุกสิ่งที่ควรเปิดสู่โลกภายนอก นี่คือสิ่งที่ คู่มือ Traefik reverse proxy สร้างขึ้นเป็นขั้นตอนถัดไปหลังจากหน้านี้ โดยใช้คอนเทนเนอร์เดียวที่ดูแลพอร์ต 80 และ 443 และทำหน้าที่ route ไปยังบริการอื่นตามชื่อโฮสต์ พร้อมกับ TLS (หากคุณมาจาก Traefik v2 แบบเก่า คู่มือการย้ายจาก Traefik v2 ไป v3 จะครอบคลุมการเปลี่ยนชื่อและการเปลี่ยนแปลงกฎต่างๆ)
ตรวจสอบการผูกพอร์ตหลังจากเริ่ม stack: sudo ss -tlnp | grep 8080 ควรแสดง 127.0.0.1:8080 ไม่ใช่ 0.0.0.0:8080 หรือ *:8080
Named volumes เทียบกับ bind mounts
db-data:/var/lib/postgresql/data คือ named volume: Docker จะสร้างและจัดการไดเรกทอรีภายใต้ /var/lib/docker/volumes/ และ mount เข้าไปในคอนเทนเนอร์ ทางเลือกอื่นคือ bind mount ซึ่งคือ ./data:/var/lib/postgresql/data ซึ่งเป็นการแมปพาธที่คุณเลือกบนโฮสต์
แนวทางปฏิบัติที่ใช้ได้จริงคือ: ใช้ named volumes สำหรับข้อมูลที่คอนเทนเนอร์เข้าถึงเท่านั้น โดยเฉพาะฐานข้อมูล เนื่องจาก Docker จะเริ่มต้นโวลุ่มด้วยสิทธิ์ความเป็นเจ้าของที่อิมเมจต้องการและสิทธิ์ของไฟล์จะทำงานได้ทันที ใช้ bind mounts สำหรับไฟล์ที่คุณแก้ไขจากโฮสต์ เช่น ไฟล์คอนฟิกที่คุณแก้ไขด้วยโปรแกรมแก้ไขข้อความ, คลังสื่อที่คุณ rsync เข้าไป หรืออะไรก็ตามที่คุณต้องการให้พาธชัดเจน ความล้มเหลวแบบคลาสสิกของ bind-mount คือเรื่องสิทธิ์ความเป็นเจ้าของ: อิมเมจรันโพรเซสด้วย UID ที่กำหนดไว้ (อิมเมจ Miniflux รันด้วย UID 65534, postgres:16-alpine ลดระดับลงไปที่ UID 70) แต่ไดเรกทอรีบนโฮสต์ของคุณเป็นเจ้าของโดย UID 1000 ทำให้แอปหยุดทำงานตอนเริ่มต้นพร้อมข้อความ permission denied ใน log ของมัน Named volumes ช่วยให้ปัญหาประเภทนี้หายไปเกือบหมด โดยแลกกับการที่ข้อมูลจะไปอยู่ในพาธที่ Docker จัดการ ซึ่งจะอธิบายไว้ด้านล่าง
environment และ .env, เก็บความลับให้ห่างจาก git
Compose จะเติมค่าใน ${POSTGRES_PASSWORD} จากไฟล์ที่ชื่อ .env ซึ่งวางอยู่ข้างๆ compose.yml แต่จะทำก็ต่อเมื่อเชลล์ของคุณไม่ได้กำหนดตัวแปรนั้นไว้ก่อน: ค่าที่ export ใน environment ของเชลล์จะมีความสำคัญเหนือกว่าไฟล์ .env ดังนั้น export POSTGRES_PASSWORD=... ที่หลงเหลือจากการทดลองก่อนหน้าอาจไปทับไฟล์โดยไม่ตั้งใจ และ docker compose config (ด้านล่าง) จะแสดงให้เห็นว่าค่าใดถูกนำไปใช้ ให้สร้างไฟล์ดังนี้:
cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignoreสร้างค่าจริงด้วย openssl rand -hex 24 ใช้เลขฐานสิบหกแทน base64 โดยตั้งใจ: เพราะรหัสผ่านนี้จะเข้าไปอยู่ในสตริงการเชื่อมต่อ DATABASE_URL และอักขระ /, + และ = ที่ base64 สร้างขึ้นจะทำให้การแยก URL ผิดพลาด ซึ่งเป็นความล้มเหลวที่แสดงออกมาเป็นข้อผิดพลาดในการยืนยันตัวตน ไม่ใช่ข้อผิดพลาดทางไวยากรณ์ และทำให้เสียเวลาทั้งคืน บรรทัด .gitignore ควรใส่ ก่อน การ commit ครั้งแรก: ไฟล์ compose นั้นปลอดภัยที่จะเผยแพร่และทำเวอร์ชัน แต่ไฟล์ .env ไม่เคยปลอดภัย และความลับใดที่หลุดเข้าไปในประวัติของ git คือความลับที่คุณต้องเปลี่ยนใหม่ หากคุณเริ่ม stack โดยขาดตัวแปร Compose จะแจ้งเตือนเสียงดังและดำเนินการต่อด้วยสตริงว่าง ซึ่งสำหรับรหัสผ่าน Postgres หมายถึงการปรับใช้ที่พัง:
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.docker compose config จะพิมพ์ไฟล์ที่แทนที่ค่าตัวแปรทั้งหมดแล้ว ซึ่งเป็นวิธีที่เร็วที่สุดในการตรวจสอบว่าคอนเทนเนอร์จะได้รับค่าอะไรจริง ๆ โปรดจำไว้ว่าผลลัพธ์ที่ได้จะรวมความลับของคุณด้วย
depends_on ไม่รออะไรทั้งสิ้น เว้นแต่คุณจะเพิ่ม healthcheck
depends_on: [db] แบบเปล่าๆ จะควบคุมเพียง ลำดับ การเริ่มเท่านั้น: Compose จะเปิด Postgres ก่อนและแอปตามมาในเสี้ยววินาทีต่อมา ในขณะที่ Postgres ยังต้องใช้เวลาอีกหลายวินาทีกว่าจะพร้อมรับการเชื่อมต่อ แอปจะพยายามเข้าถึงฐานข้อมูล ล้มเหลว และ crash หรือพยายามใหม่ ขึ้นอยู่กับว่าแอปถูกเขียนมาดีแค่ไหน
เวอร์ชันที่เชื่อถือได้คือสิ่งที่ไฟล์ข้างต้นใช้: บริการ db กำหนด healthcheck (Postgres มี pg_isready มาให้สำหรับเรื่องนี้โดยเฉพาะ) และแอปประกาศ depends_on ด้วย condition: service_healthy Compose จะเริ่มฐานข้อมูล, ตรวจสอบสถานะทุก 10 วินาที และเริ่ม Miniflux ก็ต่อเมื่อการตรวจสอบผ่านเท่านั้น หากฐานข้อมูลไม่เคยอยู่ในสถานะ healthy (เช่น รหัสผ่านผิด, โวลุ่มเสียหาย) แอปจะไม่เริ่มทำงานและ Compose จะบอกคุณว่า dependency ใดที่ล้มเหลว:
dependency failed to start: container miniflux-db-1 is unhealthyข้อความนั้นจะชี้ไปที่ docker compose logs db ซึ่งเป็นที่ที่ข้อผิดพลาดที่แท้จริงอยู่
restart: unless-stopped
restart: unless-stopped บนทั้งสองบริการหมายความว่าคอนเทนเนอร์จะกลับมาทำงานหลังจาก crash และหลังจากรีบูต VPS แต่จะยังคงหยุดทำงานหากคุณรัน docker compose stop โดยเจตนา ทางเลือกอื่นคือ always ซึ่งจะปลุกคอนเทนเนอร์ขึ้นมาใหม่แม้หลังจากหยุดด้วยตนเอง ซึ่งมักไม่ใช่สิ่งที่คุณต้องการ หากไม่มีนโยบาย restart การรีบูตเพื่ออัปเดตเคอร์เนลตอนตี 4 จะทำให้บริการของคุณหยุดทำงานเงียบๆ จนกว่าคุณจะสังเกตเห็น
คำสั่งที่ใช้เป็นประจำทุกวัน
งานประจำวันทั้งหมดประกอบด้วย 5 คำสั่ง ซึ่งต้องรันจากไดเรกทอรีของโปรเจกต์
docker compose up -d # create and start; idempotent, recreates only what changed
docker compose ps # status, ports, and health of this project's containers
docker compose logs -f miniflux # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d # upgrade to the pinned tags
docker compose down # stop and remove containers and the networkup -d สามารถรันซ้ำได้โดยไม่มีอันตราย คำสั่งนี้จะเปรียบเทียบไฟล์กับสถานะจริงและแก้ไขเฉพาะบริการที่มีการเปลี่ยนแปลง config หรือ image เท่านั้น ส่วนคู่คำสั่งอัปเกรดจะดึงข้อมูลตาม tag ที่คุณระบุไว้: สำหรับ patch release จะใช้ postgres:16-alpine ส่วนการระบุเวอร์ชันแบบเจาะจงจะไม่ถูกอัปเดตจนกว่าคุณจะแก้ไขไฟล์ ซึ่งเป็นจุดประสงค์หลักของการล็อกเวอร์ชัน เมื่อมีการอัปเกรด image เก่าจะยังคงค้างอยู่ในระบบ ให้ใช้ docker image prune -f เพื่อคืนพื้นที่ดิสก์
สำหรับคำสั่งที่ทำลายข้อมูล ซึ่งต้องระมัดระวังเป็นพิเศษ: docker compose down สามารถรันได้อย่างปลอดภัย เนื่องจาก container และ network เป็นสิ่งที่สร้างใหม่ได้ตลอดเวลา ส่วนข้อมูลของคุณจะถูกเก็บไว้ใน volume docker compose down -v จะลบ named volume ทิ้งด้วย ซึ่งหมายความว่าฐานข้อมูลของคุณจะหายไปทันทีโดยไม่มีการแจ้งเตือนให้ยืนยันและไม่สามารถกู้คืนได้ แฟล็ก -v มีไว้สำหรับการลบสภาพแวดล้อมทดสอบทิ้ง สำหรับ stack ที่มีข้อมูลจริง ให้ปฏิบัติต่อคำสั่งนี้เช่นเดียวกับที่คุณปฏิบัติต่อ rm -rf เพราะไม่มีถังขยะใน /var/lib/docker/volumes/
สำหรับการเข้าใช้งาน shell ชั่วคราวภายใน container ที่กำลังทำงานอยู่: docker compose exec db psql -U miniflux จะพาคุณเข้าสู่ฐานข้อมูล และ docker compose exec miniflux sh จะเปิด shell ในแอปพลิเคชันให้คุณ
ตำแหน่งที่ข้อมูลของคุณจัดเก็บจริง
Named volumes จะได้รับ prefix ของโปรเจกต์ ดังนั้น db-data ในไดเรกทอรีที่ชื่อ miniflux จะกลายเป็น miniflux_db-data:
docker volume ls
docker volume inspect miniflux_db-dataผลลัพธ์จาก inspect จะแสดงบรรทัดที่สำคัญ:
"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"ไดเรกทอรีดังกล่าวคือฐานข้อมูล ซึ่งมี root เป็นเจ้าของบนไฟล์ระบบของโฮสต์ และข้อมูลจะยังคงอยู่แม้ผ่านการ down, การอัปเกรด และการสร้างคอนเทนเนอร์ใหม่ นอกจากนี้ยังเป็นส่วนที่คุณต้องสำรองข้อมูลไว้อย่างเคร่งครัด
การสำรองข้อมูล named volume
รูปแบบมาตรฐานคือการใช้ container แบบใช้แล้วทิ้ง โดย mount volume แบบอ่านอย่างเดียว (read-only) ควบคู่กับไดเรกทอรีบนโฮสต์ แล้วใช้คำสั่ง tar เพื่อสำรองข้อมูล:
docker run --rm \
-v miniflux_db-data:/data:ro \
-v "$PWD":/backup \
alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .วิธีนี้ไม่ต้องติดตั้งซอฟต์แวร์เพิ่ม ไม่มีการทำงานค้างไว้ และการกู้คืนข้อมูลก็ทำในลักษณะเดียวกัน โดยใช้ tar xzf ลงใน volume เปล่าที่สร้างใหม่ด้วยการ mount แบบย้อนกลับ
ข้อควรระวังสำหรับฐานข้อมูล: การใช้ tar กับไดเรกทอรีข้อมูลของ Postgres ในขณะที่ กำลังทำงานอยู่ อาจทำให้ได้ข้อมูลในสถานะระหว่างการเขียน ซึ่งอาจไม่สามารถเริ่มทำงานได้อย่างสมบูรณ์ คุณควรเลือกใช้ docker compose stop ในช่วงเวลาที่ tar ทำงาน หรือวิธีที่ดีกว่าคือการทำ logical dump ซึ่งมีความสอดคล้องของข้อมูลโดยธรรมชาติ:
docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gzแฟล็ก -T จะปิดการใช้งาน pseudo-terminal ที่ Compose จัดสรรให้โดยอัตโนมัติ เนื่องจากการส่ง output ของ dump ผ่าน TTY อาจทำให้ข้อมูลเสียหายได้ ควรตั้งค่าคำสั่งเหล่านี้ใน cron และคัดลอกผลลัพธ์ออกจาก VPS เพราะการเก็บสำรองข้อมูลไว้บนดิสก์ลูกเดียวกับข้อมูลต้นฉบับถือเป็นเพียงการทำสำเนา ไม่ใช่การสำรองข้อมูล คู่มือ Nextcloud ได้สร้างกระบวนการสำรองข้อมูลตามกำหนดการที่สมบูรณ์โดยใช้รูปแบบทั้งสองนี้เป็นหลัก
รูปแบบความล้มเหลวและข้อความที่คุณจะพบ
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock หมายความว่าคุณยังไม่ได้อยู่ในกลุ่ม docker หรืออยู่ในกลุ่มแล้วแต่ session ปัจจุบันยังไม่ได้รับสิทธิ์นั้น id จะแสดงกลุ่มที่คุณสังกัดอยู่จริง ส่วน newgrp docker จะแก้ไขสิทธิ์ใน shell ปัจจุบันให้ทันที แต่การออกจากระบบแล้วล็อกอินใหม่จะแก้ไขสิทธิ์ให้ครอบคลุมทุกส่วน
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? เป็นปัญหาที่ต่างออกไป คือตัว daemon เองไม่ได้ทำงาน sudo systemctl status docker และ sudo journalctl -u docker -n 50 จะบอกสาเหตุให้ทราบ บน VPS สาเหตุที่พบบ่อยคือดิสก์เต็ม ให้ตรวจสอบด้วย df -h /var/lib/docker ก่อน
Bind for 127.0.0.1:8080 failed: port is already allocated หมายความว่ามี container อื่นจองพอร์ตบน host นั้นไปแล้ว docker ps จะแสดงให้เห็นว่า container ใดเป็นผู้ใช้พอร์ตนั้น สาเหตุส่วนใหญ่มักเกิดจาก container เก่าที่ค้างอยู่จากการทดลองเมื่อ docker run สัปดาห์ก่อน หาก docker ps ไม่พบสิ่งผิดปกติ อาจเป็นกระบวนการที่ไม่ใช่ Docker ที่ถือพอร์ตนั้นอยู่ ให้ใช้ sudo ss -tlnp | grep 8080 เพื่อระบุชื่อกระบวนการดังกล่าว
yaml: line 14: did not find expected key คือข้อผิดพลาดจากการเยื้องบรรทัด (indentation) ที่บรรทัดดังกล่าวหรือบรรทัดก่อนหน้า ไฟล์ Compose ใช้รูปแบบ YAML ซึ่งต้องใช้การเยื้องด้วยช่องว่าง 2 ช่องเท่านั้น ห้ามใช้ tab โดยเด็ดขาดเพราะจะทำให้เกิดข้อผิดพลาดทันที docker compose config จะช่วยตรวจสอบความถูกต้องของไฟล์โดยไม่ต้องเริ่มการทำงานของบริการใดๆ การรันคำสั่งนี้หลังแก้ไขไฟล์ทุกครั้งเป็นนิสัยที่ดีและทำได้ง่าย
ความประหลาดใจจาก ufw คือการที่ระบบไม่แสดงข้อผิดพลาดใดๆ เลย ซึ่งเป็นสิ่งที่ทำให้มันอันตราย เพราะการ deploy จะดูเหมือนสำเร็จ ufw status จะแสดงสถานะที่ดูถูกต้อง แต่การสแกนพอร์ตจากภายนอกจะยังคงพบฐานข้อมูลของคุณอยู่ ให้กลับไปอ่านส่วนของพอร์ตด้านบน ตรวจสอบทุกรายการใน ports: ว่ามีการระบุ prefix 127.0.0.1: ครบถ้วนหรือไม่ และยืนยันผลจากเครื่องอื่นด้วย curl http://your-vps-ip:8080 โดยผลลัพธ์ที่คุณต้องการคือ connection refused
จากจุดนี้ คู่มือ Traefik จะช่วยเปลี่ยน stack เดียวนี้ให้รองรับหลายแอปผ่านจุดรับ HTTPS เดียว และ สิ่งที่น่า self-host ในปี 2026 คือรายการแอปที่คุณสามารถนำไปรันผ่าน proxy ดังกล่าวได้ เมื่อคุณรันหลาย stack และแต่ละแอปมีหน้าล็อกอินแยกกัน เซิร์ฟเวอร์ SSO แบบ self-hosted อย่าง Authentik จะช่วยรวมการล็อกอินทั้งหมดให้เหลือเพียงบัญชีเดียวภายใต้ proxy ตัวเดียวกัน
เซิร์ฟเวอร์เกมอย่าง Minecraft server บน VPS เป็นโปรเจกต์ Compose แรกที่เหมาะสำหรับการฝึกฝน หากคุณต้องการเรียนรู้จากสิ่งที่ต้องใช้งานทุกวัน openGym ซึ่งเป็นแอปติดตามการออกกำลังกายแบบ self-hosted เป็น stack ขนาดเล็กที่ยึดตาม git tag แทนที่จะเป็น image tag และต้องการ TLS อยู่หน้าแอปก่อนที่คุณจะลงทะเบียน passkey แรก รูปภาพมักเป็นสิ่งแรกที่ผู้คนต้องการย้ายออกจาก cloud ของผู้ให้บริการรายอื่น และ การเปรียบเทียบระหว่าง PhotoPrism กับ Immich จะช่วยให้คุณเข้าใจความต้องการขั้นต่ำของ RAM และขั้นตอนการสำรองข้อมูลที่คุณต้องเตรียมตัวก่อนจะเริ่มใช้งาน volume ของแอปเหล่านี้ เมื่อการรันสองบริการเริ่มรู้สึกว่าไม่เพียงพอ การติดตั้ง AFFiNE เพื่อใช้เป็นพื้นที่ทำงานแบบ Notion จะใช้รูปแบบเดียวกันกับ 4 containers ซึ่งเป็นการทดสอบที่ดีว่าคุณได้สร้างนิสัยในการใช้ pinned tags, healthchecks และ named volumes ตามที่กล่าวมาข้างต้นจนคล่องแคล่วแล้วหรือไม่
FAQ
ทำไมถึงได้รับข้อความ "permission denied while trying to connect to the Docker daemon socket"?
ผู้ใช้ของคุณไม่ได้อยู่ในกลุ่ม docker หรือเพิ่งถูกเพิ่มหลังจากเริ่ม session ปัจจุบัน ซึ่งสิทธิ์จะใช้งานได้ก็ต่อเมื่อล็อกอินใหม่ ให้รัน sudo usermod -aG docker $USER จากนั้นใช้ newgrp docker หรือล็อกเอาต์แล้วล็อกอินใหม่ แล้วตรวจสอบด้วย id กลุ่มนี้ให้สิทธิ์เทียบเท่า root บนโฮสต์ ดังนั้นควรเพิ่มเฉพาะผู้ใช้ที่คุณอนุญาตให้ใช้ sudo เท่านั้น
คำสั่ง docker compose down จะลบข้อมูลของฉันหรือไม่?
คำสั่ง docker compose down ปกติจะไม่ลบข้อมูล โดยจะลบเพียง container และ network ของโปรเจกต์เท่านั้น ส่วน named volume จะยังคงอยู่และถูกเชื่อมต่อใหม่ในการรัน up -d ครั้งถัดไป ส่วน docker compose down -v คือคำสั่งที่ทำลายข้อมูล: มันจะลบ named volume รวมถึงฐานข้อมูลของคุณโดยไม่มีการยืนยันและไม่สามารถกู้คืนได้ ห้ามรัน -v กับ stack ที่มีข้อมูลจริงหากคุณไม่มีการสำรองข้อมูลที่ตรวจสอบแล้ว
docker-compose กับ docker compose ต่างกันอย่างไร?
docker-compose (แบบมีขีดกลาง) คือวิธีเรียกใช้ Compose แบบเก่า บนเซิร์ฟเวอร์รุ่นเก่ามักเป็น Compose v1 ซึ่งเป็นต้นฉบับที่เขียนด้วย Python และ Docker ได้ประกาศยุติการสนับสนุนแล้ว Docker ยังคงเผยแพร่ binary docker-compose แบบ standalone ที่เขียนด้วย Go แต่ระบุว่าไม่แนะนำให้ใช้และรองรับไว้เพื่อความเข้ากันได้กับระบบเก่าเท่านั้น จึงควรหลีกเลี่ยงบนเซิร์ฟเวอร์ใหม่ ส่วน docker compose (แบบเว้นวรรค) คือ Compose ปัจจุบัน ตั้งแต่ v2 เป็นต้นไปและนับเป็น v5 ตั้งแต่เดือนธันวาคม 2025 เป็น Go plugin สำหรับ Docker CLI ซึ่งติดตั้งเป็น docker-compose-plugin จาก apt repository ของ Docker คำสั่งและ YAML มีความเข้ากันได้เกือบสมบูรณ์ ดังนั้นเมื่อบทความเก่าบอกให้ใช้ docker-compose up ให้พิมพ์ docker compose up แทน
ทำไมฉันถึงเข้าถึง Docker container จากอินเทอร์เน็ตได้ ทั้งที่ ufw บล็อกพอร์ตไว้?
เนื่องจาก Docker เผยแพร่พอร์ตด้วยกฎ DNAT ใน chain PREROUTING ของ iptables และแพ็กเกจที่ถูกเขียนใหม่จะเดินทางผ่านเส้นทาง FORWARD ซึ่งเป็น chain ของ Docker เอง จึงไม่ผ่าน chain INPUT ที่กฎของ ufw บังคับใช้ ดังนั้น ufw deny 8080 จึงไม่มีผลกับพอร์ตของ container ที่ถูกเผยแพร่ วิธีแก้ไขคือเผยแพร่พอร์ตไปที่ 127.0.0.1: และเข้าถึงบริการผ่าน reverse proxy แทน
ควรใช้ named volume หรือ bind mount?
ใช้ named volume สำหรับข้อมูลที่ container จัดการเท่านั้น โดยเฉพาะฐานข้อมูล เพราะ Docker จะตั้งค่าความเป็นเจ้าของไฟล์ตามที่ image ต้องการและสิทธิ์การเข้าถึงจะทำงานได้ทันที ใช้ bind mount สำหรับไฟล์ที่คุณต้องจัดการจากฝั่งโฮสต์ด้วย เช่น ไฟล์ config ที่ต้องแก้ไข, สื่อที่อัปโหลด หรือไฟล์ใดๆ ที่คุณต้องการให้ระบุ path ได้ชัดเจน หาก container เริ่มทำงานไม่ได้ด้วยข้อผิดพลาด permission denied บน bind mount สิ่งแรกที่ควรตรวจสอบคือความไม่ตรงกันของ UID ระหว่างโฮสต์และ container