SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-27

วิธีติดตั้ง Docker Compose บน Ubuntu 24.04 สำหรับ VPS

เรียนรู้วิธีติดตั้ง Docker Engine และ Compose v2 บน Ubuntu 24.04 พร้อมตัวอย่างไฟล์ compose.yml สองบริการ แก้ปัญหาพอร์ตทะลุผ่าน ufw และวิธีสำรองข้อมูล volume อย่างถูกต้องและปลอดภัย

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

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

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

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

ติดตั้งจาก repository ของ Docker ไม่ใช่ของ Ubuntu

มีสองสิ่งที่ควรหลีกเลี่ยงก่อนเริ่มคำสั่งแรก แพ็กเกจ docker.io ของ Ubuntu ใช้งานได้จริง แต่เวอร์ชันมักจะล้าหลังกว่าของ Docker และไม่มีโครงสร้าง plugin ที่ซอฟต์แวร์อื่นคาดหวัง นอกจากนี้ ไบนารี docker-compose แบบ standalone ที่มีเครื่องหมายขีดคั่นคือ Compose v1 ซึ่งเขียนด้วย Python และสิ้นสุดการสนับสนุนไปตั้งแต่ปี 2023 ซึ่งเป็นสาเหตุที่ทำให้บทความสอนใช้งานเก่าๆ ใช้งานไม่ได้ ในปัจจุบัน 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

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

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

สองคำสั่งแรกจะแสดงเวอร์ชันออกมา ส่วน Docker Compose version v2.x.x จะยืนยันว่าคุณมี plugin นี้อยู่ ไม่ใช่ไบนารี v1 ที่เลิกใช้งานแล้ว การรัน 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 อยู่ในไดเรกทอรีของตนเอง ชื่อไดเรกทอรีจะกลายเป็นชื่อโปรเจกต์ ซึ่งจะถูกนำไปใช้เป็น prefix สำหรับคอนเทนเนอร์, เน็ตเวิร์ก และโวลุ่ม:

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 จะทำการ resolve ใหม่ไปยังสิ่งที่ผู้ดูแลอิมเมจเพิ่ง push ล่าสุดทุกครั้งที่คุณสั่ง pull เมื่อรวมกับนิสัยการอัปเกรดตามปกติที่คุณกำลังจะเรียนรู้ใน docker compose pull && docker compose up -d จะทำให้ :latest หมายความว่าเวอร์ชันหลัก (major version) จะถูกอัปเกรดทันทีที่ต้นทางปล่อยออกมา ไม่ใช่เมื่อคุณเลือกเอง สำหรับ PostgreSQL นี่ไม่ใช่เรื่องสมมติ: การกระโดดข้ามเวอร์ชันจาก 16 ไป 17 โดยไม่ตั้งตัวจะทำให้คอนเทนเนอร์ติดอยู่ในลูป crash เพราะข้อมูลในไดเรกทอรีไม่รองรับกัน ซึ่ง Postgres เวอร์ชันหลักต้องใช้วิธี dump และ restore ไม่ใช่แค่การ restart

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

Publish ไปที่ 127.0.0.1 เพราะ Docker ข้ามการทำงานของ ufw

รูปแบบคือ "127.0.0.1:8080:8080", host address, host port, container port บทเรียนส่วนใหญ่เขียน "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 สำหรับพอร์ตที่จำเป็นต้องเปิดสาธารณะ

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

ตรวจสอบการ bind หลังจากเริ่ม 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 volume สำหรับข้อมูลที่คอนเทนเนอร์เข้าถึงเท่านั้น โดยเฉพาะฐานข้อมูล เนื่องจาก Docker จะเริ่มต้นโวลุ่มด้วยสิทธิ์ความเป็นเจ้าของที่อิมเมจต้องการและสิทธิ์ของไฟล์จะทำงานได้ทันที ใช้ bind mounts สำหรับไฟล์ที่คุณแก้ไขจากโฮสต์ เช่น ไฟล์คอนฟิกที่คุณแก้ไขด้วย text editor, คลังสื่อที่คุณ rsync เข้าไป หรืออะไรก็ตามที่คุณต้องการให้พาธชัดเจน ความล้มเหลวคลาสสิกของ bind-mount คือเรื่องสิทธิ์ความเป็นเจ้าของ: คอนเทนเนอร์รันด้วย UID 999 แต่ไดเรกทอรีบนโฮสต์ของคุณเป็นของ UID 1000 ทำให้แอปหยุดทำงานตอนเริ่มต้นพร้อมข้อความ permission denied ใน log การใช้ named volume จะช่วยลดปัญหาประเภทนี้ได้เกือบทั้งหมด โดยแลกกับการที่ข้อมูลจะไปอยู่ในพาธที่ Docker จัดการ ซึ่งอธิบายไว้ด้านล่าง

environment และ .env, เก็บความลับไว้นอก 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 จะทำให้การแยก URL ผิดพลาด ซึ่งความล้มเหลวนี้จะแสดงเป็นข้อผิดพลาดในการยืนยันตัวตน (authentication error) ไม่ใช่ข้อผิดพลาดทางไวยากรณ์ (syntax error) และทำให้เสียเวลาแก้ไขทั้งคืน บรรทัด .gitignore ควรใส่ ก่อน การ commit ครั้งแรก: ไฟล์ compose ปลอดภัยที่จะเผยแพร่และทำเวอร์ชัน แต่ไฟล์ .env ไม่ควรทำ และความลับใดที่หลุดเข้าไปในประวัติของ git คือความลับที่คุณต้องเปลี่ยนใหม่ หากคุณเริ่ม stack โดยที่ตัวแปรหายไป Compose จะแจ้งเตือนเสียงดังและดำเนินการต่อด้วยค่าว่าง ซึ่งสำหรับรหัสผ่าน Postgres หมายถึงการ deploy ที่ล้มเหลว:

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 การรีบูตเพื่ออัปเดต kernel ตอนตี 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 network

up -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 แสดงผลว่าว่างเปล่า แสดงว่ามี process อื่นที่ไม่ใช่ Docker กำลังถือพอร์ตนั้นอยู่ ให้ใช้ sudo ss -tlnp | grep 8080 เพื่อระบุชื่อ process ดังกล่าว

yaml: line 14: did not find expected key เกิดจากข้อผิดพลาดในการเยื้องบรรทัดที่บรรทัดที่ระบุหรือบรรทัดก่อนหน้า ไฟล์ 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 คือรายการแอปพลิเคชันที่คุณควรลองนำมาใช้งาน เมื่อคุณรัน stack เหล่านี้หลายรายการและแต่ละรายการมีหน้าล็อกอินของตัวเอง เซิร์ฟเวอร์ SSO แบบ self-hosted เช่น Authentik จะช่วยรวมการล็อกอินทั้งหมดให้เหลือเพียงบัญชีเดียวที่อยู่หลัง proxy ตัวเดียวกัน

Game server เช่น Minecraft server บน VPS เป็นโปรเจกต์ Compose แรกที่เหมาะสำหรับฝึกใช้งาน หากต้องการเรียนรู้จากสิ่งที่เปิดใช้ทุกวัน openGym ซึ่งเป็น workout tracker แบบ self-hosted เป็น stack ขนาดเล็กที่ pin ไว้กับ git tag แทน image tag และต้องมี TLS อยู่ด้านหน้าก่อนลงทะเบียน passkey แรก

Photos มักเป็นข้อมูลกลุ่มแรกที่ผู้ใช้ต้องการนำกลับออกจาก cloud ของผู้อื่น และ การเปรียบเทียบ PhotoPrism กับ Immich จะช่วยกำหนด RAM ขั้นต่ำและแนวทาง backup ที่ต้องใช้ ก่อนตัดสินใจผูก volume เข้ากับบริการใดบริการหนึ่ง เมื่อมี 2 services แล้วไม่เพียงพอ การติดตั้ง AFFiNE เป็น workspace รูปแบบเดียวกับ Notion จะใช้รูปแบบเดิมกับ 4 containers และเป็นการทดสอบที่เหมาะสมว่า pinned tags, healthchecks และ named volumes ที่กล่าวข้างต้นกลายเป็นความเคยชินแล้วหรือไม่

FAQ

ทำไมฉันถึงได้รับข้อความ "permission denied while trying to connect to the Docker daemon socket"?

ผู้ใช้ของคุณไม่ได้อยู่ในกลุ่ม docker หรือถูกเพิ่มเข้ากลุ่มหลังจากเริ่มเซสชันปัจจุบันไปแล้ว เนื่องจากสิทธิ์การเป็นสมาชิกกลุ่มจะมีผลเฉพาะตอนล็อกอินเท่านั้น ให้รันคำสั่ง sudo usermod -aG docker $USER จากนั้นใช้ newgrp docker หรือล็อกเอาต์แล้วล็อกอินใหม่ แล้วตรวจสอบผลด้วย id กลุ่มนี้ให้สิทธิ์เทียบเท่า root บนโฮสต์ ดังนั้นควรเพิ่มเฉพาะผู้ใช้ที่คุณไว้วางใจให้ใช้ sudo ได้เท่านั้น

คำสั่ง docker compose down จะลบข้อมูลของฉันหรือไม่?

การใช้ docker compose down แบบปกติจะไม่ลบข้อมูล โดยคำสั่งนี้จะลบเฉพาะคอนเทนเนอร์และเน็ตเวิร์กของโปรเจกต์เท่านั้น ส่วน named volumes จะยังคงอยู่และถูกเชื่อมต่อกลับมาใหม่เมื่อรัน up -d ในครั้งถัดไป ส่วน docker compose down -v คือรูปแบบที่ทำลายข้อมูล: มันจะลบ named volumes รวมถึงฐานข้อมูลของคุณทิ้งโดยไม่มีการยืนยันและไม่สามารถกู้คืนได้ ห้ามรัน -v กับ stack ที่มีข้อมูลจริงเด็ดขาด เว้นแต่คุณจะมีสำรองข้อมูลที่ตรวจสอบแล้วว่าใช้งานได้จริง

docker-compose กับ docker compose ต่างกันอย่างไร?

docker-compose (มีขีดกลาง) คือ Compose v1 ซึ่งเป็นไบนารี Python แบบแยกส่วนที่สิ้นสุดอายุการใช้งาน (EOL) ไปในปี 2023 และไม่ควรติดตั้งบนเซิร์ฟเวอร์ใหม่ ส่วน docker compose (เว้นวรรค) คือ Compose v2 ซึ่งเป็นปลั๊กอินภาษา Go สำหรับ 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 จึงไม่มีผลใดๆ ต่อพอร์ตของคอนเทนเนอร์ที่ถูกเผยแพร่ออกไป ให้แก้ไขที่ต้นเหตุแทน: โดยการเผยแพร่พอร์ตไปยัง 127.0.0.1: และให้บริการผ่าน reverse proxy แทน

ฉันควรใช้ named volume หรือ bind mount?

ควรใช้ named volumes สำหรับข้อมูลที่คอนเทนเนอร์เข้าถึงเพียงอย่างเดียว โดยเฉพาะฐานข้อมูล เพราะ Docker จะตั้งค่าความเป็นเจ้าของไฟล์ให้ตรงกับที่อิมเมจต้องการและสิทธิ์การเข้าถึงจะทำงานได้ทันที ส่วน bind mounts เหมาะสำหรับไฟล์ที่คุณต้องจัดการจากฝั่งโฮสต์ด้วย เช่น ไฟล์คอนฟิกที่คุณแก้ไข, สื่อที่คุณอัปโหลด หรืออะไรก็ตามที่คุณต้องการให้ระบุ path ได้ชัดเจน หากคอนเทนเนอร์ไม่สามารถเริ่มทำงานได้โดยแจ้งข้อผิดพลาด permission denied บน bind mount สิ่งแรกที่ควรตรวจสอบคือความไม่ตรงกันของ UID ระหว่างโฮสต์กับคอนเทนเนอร์