SSD Nodes Learn
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-07-24

วิธีติดตั้ง Docker Compose บน Ubuntu 24.04

สอนติดตั้ง Docker Engine และ Compose v2 บน Ubuntu 24.04 พร้อมวิธีเขียน compose.yml แบบ 2 services และวิธีตั้งค่า ufw เพื่อป้องกันปัญหาเรื่องการเปิด port

สิ่งที่คุณกำลังจะสร้าง

Docker Compose เป็นโครงสร้างพื้นฐานสำหรับเกือบทุกบริการในเว็บไซต์นี้ ไม่ว่าจะเป็น Nextcloud, Vaultwarden, n8n, Immich หรือ Rocket.Chat ทุกคู่มือจะเริ่มต้นด้วยคำสั่ง "write this compose file" และหน้านี้จะอธิบายความหมายของไฟล์ดังกล่าว คุณจะได้ติดตั้ง Docker Engine และ Compose v2 plugin จาก apt repository ของ Docker บน Ubuntu 24.04 จากนั้นจะทำการติดตั้ง stack ที่ประกอบด้วย 2 service คือ Miniflux ซึ่งเป็น RSS reader ขนาดเล็ก และ PostgreSQL เนื่องจากคู่บริการนี้ครอบคลุมทุกรูปแบบที่แอปพลิเคชันขนาดใหญ่ใช้งาน ได้แก่ การระบุเวอร์ชันของ image, การใช้ database ที่มี healthcheck, การใช้ named volume, การใช้ secrets ในไฟล์ .env และการเปิด port เฉพาะ localhost เท่านั้น

ขั้นตอนการติดตั้งใช้เวลาประมาณ 5 นาที ส่วนที่เหลือของคู่มือนี้จะครอบคลุมประเด็นที่อาจก่อให้เกิดปัญหาในภายหลัง ได้แก่ การที่ docker group มีสิทธิ์เทียบเท่า root, การที่ published ports สามารถข้ามผ่านกฎของ ufw ได้โดยตรง และ flag หนึ่งตัวใน docker compose down ที่สามารถลบ database ได้ทันทีโดยไม่มีการถามยืนยัน

สิ่งที่ต้องเตรียม: Ubuntu 24.04 KVM VPS ที่ติดตั้งใหม่, user ที่มีสิทธิ์ sudo และ RAM อย่างน้อย 1 GB หากมีการติดตั้ง Docker ไว้ก่อนหน้านี้แล้วสามารถใช้งานต่อได้ โดยส่วนแรกของคู่มือจะอธิบายวิธีการลบเวอร์ชันเก่าออก

ติดตั้งจาก repo ของ Docker แทนที่จะใช้ของ Ubuntu

ต้องหลีกเลี่ยงความผิดพลาด 2 ประการก่อนเริ่มใช้คำสั่งแรก แพ็กเกจ docker.io ของ Ubuntu ใช้งานได้ แต่เวอร์ชันจะล้าหลังกว่า Docker และมีโครงสร้าง plugin ไม่ตรงตามที่ระบบอื่นต้องการ นอกจากนี้ไฟล์ binary docker-compose (ที่มีเครื่องหมาย hyphen) คือ Compose v1 ซึ่งเขียนด้วย Python และสิ้นสุดอายุการใช้งาน (end-of-life) ไปตั้งแต่ปี 2023 ซึ่งเป็นสาเหตุที่ทำให้ tutorial รุ่นเก่าใช้งานไม่ได้ ปัจจุบัน Compose คือ docker compose (แบบเว้นวรรค) ซึ่งเป็น CLI plugin ที่ติดตั้งจาก repository เดียวกับ engine

หากมีซอฟต์แวร์เหล่านี้อยู่ในเครื่องแล้ว ให้ลบออกก่อน รวมถึง docker-compose-v2 ซึ่งเป็นแพ็กเกจ plugin ของ Ubuntu เพื่อให้ทุกอย่างมาจาก repository เดียวกัน:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

Package '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

ตรวจสอบทั้ง 3 ส่วน:

docker --version
docker compose version
sudo docker run --rm hello-world

สองคำสั่งแรกจะแสดงเลขเวอร์ชัน — Docker Compose version v2.x.x จะยืนยันว่าคุณมี plugin ไม่ใช่ไฟล์ binary v1 ที่เลิกใช้งานแล้ว ส่วนการรัน hello-world ควรจบด้วย Hello from Docker! แพ็กเกจนี้จะเปิดใช้งาน service เมื่อ boot เครื่อง และ 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

การเป็นสมาชิกกลุ่มจะมีผลเมื่อมีการ login ใหม่ ดังนั้นข้อผิดพลาดจะยังคงปรากฏใน shell ปัจจุบัน ให้รัน newgrp docker สำหรับ session นี้ หรือ log out แล้ว log in ใหม่ จากนั้น id ควรแสดงรายการ docker ในกลุ่มของคุณ

ส่วนที่สำคัญที่สุดคือ: การเป็นสมาชิกของกลุ่ม docker มีสิทธิ์เทียบเท่า root บน host ไม่ใช่แค่ "ใกล้เคียง root" หรือ "สิทธิ์ระดับสูง" แต่คือ root ผู้ใดก็ตามที่อยู่ในกลุ่มนี้สามารถรัน docker run --rm -it -v /:/host alpine chroot /host และเข้าควบคุม filesystem ทั้งหมดได้โดยไม่ต้องใช้รหัสผ่าน กลุ่มนี้ถูกสร้างขึ้นเพื่อความสะดวก ไม่ใช่เพื่อการจำกัดสิทธิ์ (containment)

โหมด rootless ของ Docker คือทางเลือกที่แท้จริง โดย daemon จะทำงานภายใต้ user ที่ไม่มีสิทธิ์พิเศษของคุณ ซึ่งมีข้อเสียคือ: พอร์ตที่ต่ำกว่า 1024 ต้องมีการตั้งค่าเพิ่มเติม, การทำงานของ network ต้องผ่าน userspace shim ซึ่งทำให้เกิด overhead และ image บางตัวอาจทำงานผิดพลาดหากไม่มีสิทธิ์ root จริง สำหรับ VPS ที่มีผู้ดูแลระบบเพียงคนเดียวและมีสิทธิ์ sudo อยู่แล้ว การใช้กลุ่มนี้จะไม่มีความแตกต่างในทางปฏิบัติ และเป็นสิ่งที่คู่มือทุกฉบับในที่นี้ใช้เป็นมาตรฐาน — แต่อย่าแจกจ่ายสิทธิ์นี้ให้ผู้อื่นเสมือนว่ามันมีระดับความปลอดภัยน้อยกว่า sudo

Anatomy of a compose file

กำหนด directory แยกสำหรับแต่ละ stack โดยชื่อ directory จะกลายเป็นชื่อ project ซึ่งจะถูกนำไปใช้เป็น prefix สำหรับ containers, networks และ volumes:

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

สร้าง compose.yml (ชื่อเรียกปัจจุบัน; docker-compose.yml ยังใช้งานได้) ให้ข้ามการใช้ key 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:

ทุกบรรทัดข้างต้นคือการตัดสินใจ ให้พิจารณาไปทีละขั้นตอน

Pin image versions — :latest plus a pull is an unattended upgrade

ใช้ postgres:16-alpine แทนที่จะเป็น postgres:latest เนื่องจาก tag ไม่ได้ถูกตรึงไว้: :latest จะถูก resolve ไปยังเวอร์ชันล่าสุดที่ maintainer push ขึ้นไปทุกครั้งที่มีการ pull เมื่อรวมสิ่งนี้เข้ากับนิสัยการอัปเกรดตามรอบที่กำลังจะได้เรียนรู้คือ docker compose pull && docker compose up -d จะทำให้ :latest หมายถึงการข้ามไปยัง major-version ตามที่ upstream ปล่อยออกมา ไม่ใช่ตามที่คุณเลือก สำหรับ PostgreSQL เรื่องนี้ไม่ใช่เรื่องสมมติ: การข้ามเวอร์ชันจาก 16 ไป 17 อย่างกะทันหันจะทำให้ container เกิดอาการ crash-loop เนื่องจาก directory ข้อมูลไม่รองรับ เพราะการอัปเกรด major version ของ Postgres ต้องใช้วิธี dump และ restore ไม่ใช่แค่การ restart

ควร pin อย่างน้อยในระดับ major version (postgres:16-alpine สำหรับ patch releases ของ 16.x) และ pin application ไปยัง release ที่แน่นอนอย่าง miniflux/miniflux:2.2.9 โดยตรวจสอบจากหน้า releases ของโปรเจกต์และใช้เวอร์ชันปัจจุบันขณะเขียนไฟล์ การอัปเกรดจะกลายเป็นการแก้ไขเพียงบรรทัดเดียวที่คุณตั้งใจทำ และสามารถตรวจสอบได้ใน git diff

Publish to 127.0.0.1, because Docker walks around ufw

"127.0.0.1:8080:8080" — host address, host port, container port การเขียน tutorial ส่วนใหญ่จะใช้ "8080:8080" ซึ่งเป็นตัวย่อของ 0.0.0.0:8080:8080 คือการ listen บนทุก interface รวมถึง public interface ด้วย

นี่คือกับดักที่เกือบทุกคนต้องเคยเจอ Docker ทำการ publish port โดยการเขียน DNAT rule เพื่อเขียน destination ของ packet ใหม่ให้เป็น internal IP ของ container ก่อน การทำ filtering ดังนั้น packet จะวิ่งผ่านเส้นทาง FORWARD และไม่ผ่าน INPUT ซึ่งเป็นที่อยู่ของ ufw rules ผลคือ sudo ufw deny 8080 จะรายงานว่าสำเร็จ, ufw status จะแสดงว่า port ถูกปฏิเสธ แต่ service ยังคงตอบสนองต่ออินเทอร์เน็ตทั้งหมด Firewall ของคุณไม่ได้เสีย แต่ถูกข้ามผ่านโดยการออกแบบ Why Docker bypasses ufw, and how to filter container traffic for real จะอธิบายกลไกนี้และวิธีแก้ไขด้วย DOCKER-USER สำหรับ port ที่ต้องการให้เป็น public

วิธีแก้ปัญหาที่ถาวรคือ: bind ports ที่ publish เข้ากับ 127.0.0.1 เว้นแต่จะมีเหตุผลเฉพาะเจาะจง และใช้ reverse proxy วางไว้ด้านหน้าสำหรับบริการที่ต้องเปิดสู่โลกภายนอก นั่นคือสิ่งที่ Traefik reverse proxy guide จะสอนในขั้นตอนถัดไป — คือการใช้ container เดียวที่ถือ port 80 และ 443 และทำหน้าที่ route ไปยังบริการอื่น ๆ ผ่าน hostname พร้อม TLS (หากคุณใช้ Traefik v2 อยู่เดิม: Traefik v2 to v3 migration guide จะครอบคลุมเรื่องการเปลี่ยนชื่อและกฎเกณฑ์ต่าง ๆ)

ตรวจสอบการ bind หลังจากเริ่ม stack: sudo ss -tlnp | grep 8080 ควรแสดง 127.0.0.1:8080 ไม่ใช่ 0.0.0.0:8080 หรือ *:8080

Named volumes vs bind mounts

db-data:/var/lib/postgresql/data คือ named volume: Docker จะสร้างและจัดการ directory ภายใต้ /var/lib/docker/volumes/ และ mount เข้าไปใน container ส่วนอีกวิธีคือ bind mount (./data:/var/lib/postgresql/data) ซึ่งเป็นการ map path ที่คุณเลือกบน host

ข้อแตกต่างในการใช้งานจริง: ใช้ named volumes สำหรับ container ที่จัดการข้อมูลเท่านั้น — โดยเฉพาะ database เนื่องจาก Docker จะ initialize volume ด้วย ownership ตามที่ image กำหนดและ file permissions จะใช้งานได้ทันที ใช้ bind mounts สำหรับไฟล์ที่คุณต้องแก้ไขจาก host — เช่น config files ที่คุณแก้ไขด้วย text editor, media library ที่คุณ rsync เข้าไป หรือไฟล์ใด ๆ ที่คุณต้องการให้ path ชัดเจน ปัญหาคลาสสิกของ bind-mount คือเรื่อง ownership: container รันด้วย UID 999 แต่ directory บน host เป็นของ UID 1000 ทำให้แอปพลิเคชันตายตอนเริ่มต้นพร้อม error permission denied ใน log การใช้ named volumes จะช่วยลดปัญหาประเภทนี้ได้เกือบทั้งหมด แต่ต้องแลกมาด้วยการที่ข้อมูลถูกเก็บไว้ใน path ที่ Docker จัดการ (รายละเอียดด้านล่าง)

environment and .env — keep secrets out of git

${POSTGRES_PASSWORD} ไม่ได้อ่านค่าจาก shell ของคุณ; Compose จะดึงค่ามาจากไฟล์ชื่อ .env ที่วางอยู่ข้าง ๆ compose.yml ให้สร้างไฟล์ดังนี้:

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 แนะนำให้ใช้แบบ hex แทน base64: เนื่องจากรหัสผ่านนี้จะถูกนำไปใส่ใน connection string ของ DATABASE_URL และตัวอักษร /, + และ = ที่ base64 สร้างขึ้นจะทำให้การ parse URL เสียหาย ซึ่งจะแสดงผลเป็น error การยืนยันตัวตน (authentication error) ไม่ใช่ syntax error และทำให้เสียเวลาแก้ไขนาน บรรทัด .gitignore ควรถูกใส่ไว้ ก่อน การ commit ครั้งแรก: ไฟล์ compose จะปลอดภัยสำหรับการ publish และทำ version control แต่ไฟล์ .env ไม่ควรทำเช่นนั้น และความลับใด ๆ ที่หลุดเข้าไปใน git history จะต้องถูกเปลี่ยนใหม่ (rotate) หากคุณเริ่มรัน stack โดยที่ตัวแปรหายไป Compose จะแจ้งเตือนและรันต่อด้วยค่าว่าง ซึ่งสำหรับรหัสผ่าน Postgres หมายถึงการ deployment ที่ล้มเหลว:

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config ใช้สำหรับพิมพ์ไฟล์ที่ถูก interpolate ค่าแล้วทั้งหมด ซึ่งเป็นวิธีที่เร็วที่สุดในการตรวจสอบว่า container จะได้รับค่าอะไรจริง ๆ; โปรดจำไว้ว่าผลลัพธ์จะรวมความลับของคุณด้วย

depends_on waits for nothing — unless you add a healthcheck

การใช้ depends_on: [db] เพียงอย่างเดียวจะควบคุมแค่ ลำดับ การเริ่มเท่านั้น: Compose จะเริ่ม Postgres ก่อนแล้วตามด้วยแอปพลิเคชันในอีกไม่กี่วินาที ในขณะที่ Postgres ยังต้องใช้เวลาอีกหลายวินาทีกว่าจะเริ่มรับ connection ได้ แอปจะพยายามเชื่อมต่อกับ database, ล้มเหลว และ crash หรือพยายามใหม่ ขึ้นอยู่กับการเขียนโปรแกรม

เวอร์ชันที่เชื่อถือได้คือแบบที่ใช้ในไฟล์ข้างต้น: service db จะกำหนด healthcheck (Postgres มี pg_isready มาให้เพื่อการนี้โดยเฉพาะ) และแอปจะประกาศ depends_on พร้อมกับ condition: service_healthy Compose จะเริ่ม database, ตรวจสอบสถานะทุก 10 วินาที และจะเริ่ม Miniflux ก็ต่อเมื่อการตรวจสอบผ่านเท่านั้น หาก database ไม่สามารถใช้งานได้ (เช่น รหัสผ่านผิด หรือ volume เสียหาย) แอปจะไม่เริ่มทำงานและ Compose จะแจ้งว่า dependency ใดที่ล้มเหลว:

dependency failed to start: container miniflux-db-1 is unhealthy

ข้อความนั้นจะชี้ไปยัง docker compose logs db ซึ่งเป็นจุดที่มี error ที่แท้จริงเกิดขึ้น

restart: unless-stopped

การใช้ restart: unless-stopped กับทั้งสอง service หมายความว่า container จะกลับมาทำงานใหม่หลังจาก crash หรือหลังจาก reboot เครื่อง VPS แต่จะหยุดทำงานหากคุณสั่ง docker compose stop ด้วยตัวเอง ส่วนทางเลือกอย่าง always จะปลุก container ให้กลับมาทำงานแม้จะสั่ง stop ไปด้วยมือ ซึ่งมักไม่ใช่สิ่งที่คุณต้องการ หากไม่มี restart policy การ reboot เครื่องจากการอัปเดต kernel ตอนตี 4 จะทำให้บริการของคุณหยุดทำงานไปอย่างเงียบ ๆ จนกว่าคุณจะสังเกตเห็น

คำสั่งที่ใช้เป็นประจำทุกวัน

งานประจำวันทั้งหมดประกอบด้วย 5 คำสั่ง ซึ่งต้องรันจาก project directory

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 network

up -d สามารถรันซ้ำได้โดยปลอดภัย เนื่องจากคำสั่งนี้จะเปรียบเทียบความแตกต่างระหว่างไฟล์กับสถานะปัจจุบัน และจะดำเนินการเฉพาะกับ service ที่มีการเปลี่ยนแปลง config หรือ image เท่านั้น ชุดคำสั่ง upgrade จะดึงข้อมูลตามที่ pinned tags กำหนดไว้ โดยจะดึงเฉพาะ patch releases ที่อยู่ภายใต้ postgres:16-alpine เท่านั้น และจะไม่ดึงข้อมูลใหม่หากไม่ได้แก้ไข exact pin ซึ่งเป็นวัตถุประสงค์หลักของระบบนี้ เมื่อทำการ upgrade แล้วจะมี image เก่าสะสมอยู่ ให้ใช้ docker image prune -f เพื่อคืนพื้นที่ใน disk

สำหรับคำสั่งที่อาจทำให้ข้อมูลสูญหาย: docker compose down นั้นปลอดภัย เนื่องจาก container และ network สามารถสร้างใหม่ได้เสมอ และข้อมูลของคุณถูกเก็บไว้ใน volume อย่างไรก็ตาม docker compose down -v จะลบ named volumes ด้วย ซึ่งหมายถึงข้อมูล database ของคุณจะถูกลบออกทันที โดยไม่มีการถามยืนยันและไม่สามารถเรียกคืนได้ flag -v มีไว้สำหรับลบการทดลองระบบ (experiments) สำหรับ stack ที่มีข้อมูลสำคัญ ให้ระมัดระวังการใช้งานเช่นเดียวกับ rm -rf เพราะไม่มีถังขยะสำหรับคำสั่ง /var/lib/docker/volumes/

หากต้องการเข้าใช้งาน shell ภายใน container ที่กำลังรันอยู่: docker compose exec db psql -U miniflux จะพาคุณเข้าสู่ database และ docker compose exec miniflux sh จะพาคุณเข้าสู่ shell ของแอปพลิเคชัน

ตำแหน่งที่เก็บข้อมูลจริงของคุณ

Named volumes จะได้รับ project prefix ดังนั้น db-data ใน directory ชื่อ miniflux จะกลายเป็น miniflux_db-data:

docker volume ls
docker volume inspect miniflux_db-data

ผลลัพธ์จาก inspect จะมีบรรทัดที่สำคัญดังนี้:

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

Directory ดังกล่าวคือ database ซึ่งมี root เป็นเจ้าของอยู่บน host filesystem ข้อมูลจะยังคงอยู่แม้มีการ down, การ upgrade หรือการ rebuild container และนี่คือข้อมูลชุดเดียวกับที่การ backup ของคุณต้องจัดเก็บให้ครบถ้วน

การสำรองข้อมูล named volume

รูปแบบมาตรฐานคือการใช้ container ชั่วคราวที่ mount volume แบบ read-only ไว้ข้างๆ host directory แล้วทำการ 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 .

วิธีนี้ไม่ต้องติดตั้งโปรแกรมเพิ่มเติม ไม่มีกระบวนการอื่นทำงานค้างไว้ และการ restore คือการทำ mirror image — โดยใช้ tar xzf ลงใน volume ใหม่ที่ว่างเปล่าด้วยการ mount แบบสลับกัน

ข้อควรระวังสำหรับ database: การ tar directory ข้อมูลของ Postgres ที่กำลัง running อยู่ อาจทำให้ได้ข้อมูลในสถานะระหว่างการเขียน (mid-write state) ซึ่งจะทำให้ระบบไม่สามารถ start ได้อย่างถูกต้อง วิธีแก้ไขคือให้ใช้ 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 มักจะจัดสรรให้โดย default — เนื่องจากการส่ง output ของ dump ผ่าน TTY อาจทำให้ข้อมูลเสียหาย ควรตั้งค่าคำสั่งเหล่านี้ไว้ใน cron และคัดลอกผลลัพธ์ออกไปเก็บไว้นอก VPS; การเก็บ backup ไว้ใน disk ชุดเดียวกับข้อมูลที่ต้องการป้องกัน ถือเป็นเพียงการ copy ไม่ใช่การ backup ที่แท้จริง คู่มือ Nextcloud ได้สร้างระบบการทำงานแบบกำหนดเวลา (scheduled routine) โดยใช้รูปแบบทั้งสองนี้

Failure modes, with the strings you will see

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — คุณยังไม่ได้อยู่ในกลุ่ม docker หรือคุณอยู่ในกลุ่มแล้วแต่ session ปัจจุบันยังไม่ได้อัปเดต id จะแสดงกลุ่มที่มีผลใช้งานอยู่; newgrp docker จะแก้ไข shell ปัจจุบัน การ log out และ log in ใหม่จะแก้ไขปัญหาทั้งหมด

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 port นี้อยู่แล้ว docker ps จะแสดงว่าคือ container ใด; สาเหตุส่วนใหญ่คือ container ที่ค้างจากการทดลองใช้ docker run เมื่อหลายสัปดาห์ก่อน หาก docker ps ไม่พบปัญหา แสดงว่ามี process อื่นที่ไม่ใช่ Docker ใช้งาน port อยู่: sudo ss -tlnp | grep 8080 จะระบุชื่อ process นั้น

yaml: line 14: did not find expected key — เกิดข้อผิดพลาดในการเว้นวรรค (indentation) ที่บรรทัดดังกล่าวหรือบรรทัดด้านบน Compose files ใช้รูปแบบ YAML: ต้องเว้นวรรค 2 spaces และใช้เฉพาะ space เท่านั้น การใช้ tab character จะทำให้เกิดข้อผิดพลาดทันที docker compose config ใช้สำหรับตรวจสอบความถูกต้องของไฟล์โดยไม่ต้องเริ่มการทำงาน และควรเรียกใช้ทุกครั้งหลังการแก้ไข

กรณีของ ufw จะไม่มีการแสดงข้อผิดพลาดใดๆ ซึ่งเป็นจุดที่อันตราย: การ deploy จะสำเร็จ ufw status ดูเหมือนถูกต้อง แต่การสแกน port จากภายนอกยังคงเข้าถึง database ได้ ให้กลับไปอ่านส่วน ports ด้านบน ตรวจสอบทุกรายการใน ports: ว่ามี prefix 127.0.0.1: ครบถ้วนหรือไม่ และตรวจสอบจากเครื่อง อื่น ด้วย curl http://your-vps-ip:8080 — ผลลัพธ์ที่ต้องการคือ connection refused

จากจุดนี้ คู่มือ Traefik จะเปลี่ยน stack นี้ให้กลายเป็นแอปพลิเคชันจำนวนมากที่อยู่หลัง HTTPS entry point เดียวกัน และ สิ่งที่ควรทำ self-hosting ในปี 2026 คือรายการสิ่งของที่คุณควรนำมาใช้งานผ่านระบบนี้

Game server เช่น Minecraft server บน VPS เป็นโปรเจกต์ Compose เริ่มต้นที่เหมาะสำหรับการฝึกฝน

FAQ

Why do I get "permission denied while trying to connect to the Docker daemon socket"?

Your user is not in the docker group, or was added after the current session began — membership only applies at login. Run sudo usermod -aG docker $USER, then newgrp docker or log out and back in, and confirm with id. The group grants root-equivalent access to the host, so only add users you would give sudo.

Does docker compose down delete my data?

Plain docker compose down does not — it removes containers and the project network; named volumes survive and the next up -d reattaches them. docker compose down -v is the destructive form: it deletes the named volumes, meaning your database, with no confirmation and no undo. Never run -v on a stack with real data unless you hold a verified backup.

What is the difference between docker-compose and docker compose?

docker-compose (hyphen) is Compose v1, a standalone Python binary that reached end of life in 2023 and should not be installed on new servers. docker compose (space) is Compose v2, a Go plugin for the Docker CLI, installed as docker-compose-plugin from Docker's apt repository. Commands and YAML are almost fully compatible, so when an old tutorial says docker-compose up, type docker compose up.

Why can I reach my Docker container from the internet even though ufw blocks the port?

Because Docker publishes ports with DNAT rules in iptables' PREROUTING chain, and the rewritten packets travel the FORWARD path through Docker's own chains — they never hit the INPUT chain where ufw's rules apply. ufw deny 8080 therefore does nothing to a published container port. Fix it at the source: publish to 127.0.0.1: and expose services through a reverse proxy instead.

Should I use a named volume or a bind mount?

Named volumes for data only the container touches — databases especially, since Docker sets the ownership the image expects and permissions just work. Bind mounts for files you also handle from the host: configs you edit, media you upload, anything whose path you want obvious. If a container fails at startup with permission denied on a bind mount, host-vs-container UID mismatch is the first thing to check.