วิธีติดตั้ง Planka ด้วย Docker Compose บน VPS ของคุณเอง
เรียนรู้วิธีติดตั้ง Planka บน VPS ด้วย Docker Compose พร้อมการตั้งค่า Postgres และ Traefik ครบถ้วน รวมถึงการแก้ไขปัญหา BASE_URL ที่มักทำให้เข้าสู่ระบบไม่ได้ในเวอร์ชันล่าสุด
สิ่งที่คุณจะได้รับจากการ self-host Planka
การ self-host Planka ช่วยให้ทีมของคุณมีกระดาน Kanban ที่ใช้รูปแบบการ์ด รายการ และป้ายกำกับแบบเดียวกับที่คุ้นเคยใน Trello โดยทำงานบน VPS ที่คุณควบคุมเอง ไม่มีการจำกัดจำนวนที่นั่งและไม่มีการเรียกเก็บเงินรายผู้ใช้ เนื่องจากค่าใช้จ่ายเดียวคือค่าเช่าเซิร์ฟเวอร์ คู่มือนี้จะแนะนำการติดตั้งด้วย Docker Compose โดยมี Traefik เป็น reverse proxy ใช้ Postgres สำหรับจัดเก็บข้อมูล และใช้ named volume สำหรับไฟล์ทุกไฟล์ที่ผู้ใช้อัปโหลด
ผู้อ่านที่ตั้งเป้าไว้คือทีมขนาด 2 ถึง 5 คนที่กำลังย้ายออกจากแผนใช้งานฟรีของ Trello หากคุณยังตัดสินใจไม่ได้ว่าจะใช้กระดานตัวไหน ให้ลองอ่าน การเปรียบเทียบทางเลือกของ Trello แบบ self-hosted ก่อน คู่มือนี้ถือว่าคุณตัดสินใจเลือกแล้วและจะครอบคลุมเฉพาะขั้นตอนการติดตั้งเท่านั้น
คุณต้องมี VPS ที่ติดตั้ง Docker Engine พร้อมปลั๊กอิน Compose และมี DNS A record ชี้มาที่ VPS นั้น นอกจากนี้คุณต้องมี instance ของ Traefik ที่ทำหน้าที่จัดการ TLS (transport layer security) อยู่บนเครื่องนั้นแล้ว หากยังไม่มี Traefik ให้ตั้งค่า reverse proxy ด้วย Traefik สำหรับแอป Compose หลายตัว ก่อน และอ่าน พื้นฐาน Docker Compose สำหรับ VPS หากไฟล์ด้านล่างนี้ดูไม่คุ้นเคย
Planka ต้องการทรัพยากร VPS มากน้อยเพียงใด
โครงการไม่ได้กำหนดความต้องการฮาร์ดแวร์ขั้นต่ำไว้ ดังนั้นตัวเลขใดก็ตามที่คุณพบให้ถือว่าเป็นเพียงจุดเริ่มต้น ไม่ใช่ค่ามาตรฐานที่วัดผลได้ ตัวเลข 2 vCPU และ 4 GB ที่มักพบในหน้าเว็บของผู้ให้บริการโฮสติ้งเป็นเพียงค่าเริ่มต้นที่ปลอดภัย ไม่ใช่ข้อกำหนดที่โครงการได้ทำการทดสอบมา ซึ่งถือว่ามากเกินความจำเป็นสำหรับบอร์ดที่มีผู้ใช้งานเพียง 5 คน
สิ่งที่ทำงานจริงมีขนาดเล็กมาก ได้แก่ กระบวนการ Node.js หนึ่งรายการที่ทำหน้าที่ให้บริการ API และ frontend ที่ build ไว้แล้ว และกระบวนการ Postgres หนึ่งรายการที่จัดเก็บข้อมูล นอกจากนี้ยังมีกระบวนการ proxy ขนาดเล็กอีกหนึ่งรายการที่ทำงานภายใน container ของ Planka เพื่อกรองคำขอขาออก แผนการใช้งานระดับ 1 vCPU และ 2 GB สามารถรองรับบอร์ดที่มีผู้ใช้งาน 2 ถึง 5 คนได้ โดยหน่วยความจำส่วนที่เหลือส่วนใหญ่จะถูกใช้เป็น cache ของ Postgres บอร์ดเป็นบริการที่ใช้ทรัพยากรน้อย ดังนั้นหาก VPS เดียวกันนี้ต้องใช้เก็บเอกสารของทีมด้วย ให้พิจารณาขนาดของแอปนั้นเป็นหลักก่อน: การรัน AFFiNE ในฐานะ workspace สไตล์ Notion ต้องการหน่วยความจำอีกสองสามกิกะไบต์แยกต่างหากก่อนที่ Planka จะเริ่มใช้งาน
ให้กำหนดขนาดดิสก์ก่อนกำหนดขนาดหน่วยความจำ เนื่องจากไฟล์แนบคือส่วนที่จะเพิ่มขนาดขึ้นเรื่อยๆ ให้วัดผลจาก instance ของคุณเองแทนการเชื่อถือข้อมูลในย่อหน้านี้:
docker stats --no-stream
docker system df -vคำสั่งแรกจะแสดงการใช้หน่วยความจำและ CPU แบบเรียลไทม์ของแต่ละ container ส่วนคำสั่งที่สองจะแสดงพื้นที่ที่แต่ละ volume ใช้งานอยู่ ให้เก็บข้อมูลทั้งสองค่าหลังจากผ่านสัปดาห์การทำงานปกติ ไม่ใช่ในวันที่ติดตั้ง เพราะบอร์ดที่ไม่มีการใช้งานจะไม่สามารถบอกข้อมูลการใช้งานจริงของทีมคุณได้
เขียนไฟล์ Compose
สร้างไดเรกทอรีและกำหนดสิทธิ์ความเป็นเจ้าของ เพื่อให้คุณไม่ต้องแก้ไขไฟล์เหล่านี้ผ่าน sudo อีกต่อไป
sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/plankaสร้างข้อมูลลับ (secrets) ลงในไฟล์ .env ไว้ข้างไฟล์ Compose โดย Compose จะอ่านไฟล์ดังกล่าวโดยอัตโนมัติและแทนที่ค่าต่างๆ ให้
umask 077
{
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 64)"
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)"
printf 'ADMIN_PASSWORD=%s\n' "$(openssl rand -hex 12)"
} > .env
chmod 600 .envการใช้ openssl rand -hex นั้นมีเจตนาที่ชัดเจน เนื่องจากสตริงเลขฐานสิบหกประกอบด้วยตัวเลขและตัวอักษร a ถึง f เท่านั้น จึงไม่ทำให้เกิดข้อผิดพลาดในสตริงการเชื่อมต่อ DATABASE_URL ที่นำไปวาง ในขณะที่รหัสผ่านแบบ base64 ที่มีเครื่องหมายทับหรือเครื่องหมาย at อาจทำให้เกิดข้อผิดพลาดในการเชื่อมต่อซึ่งดูเหมือนชื่อโฮสต์ผิดพลาด และนั่นจะทำให้คุณเสียเวลาตรวจสอบนานนับชั่วโมง รูปแบบที่กว้างขึ้นครอบคลุมอยู่ใน การเก็บข้อมูลลับไว้นอกไฟล์ Compose
ตอนนี้ให้ใช้ docker-compose.yml โดยแทนที่ kanban.example.com ด้วยชื่อโฮสต์ของคุณเองในทั้งสองจุดที่ปรากฏ
services:
planka:
image: ghcr.io/plankanban/planka:2.1.1
restart: unless-stopped
volumes:
- planka-data:/app/data
environment:
- BASE_URL=https://kanban.example.com
- DATABASE_URL=postgresql://planka:${POSTGRES_PASSWORD}@postgres/planka
- SECRET_KEY=${SECRET_KEY}
- TRUST_PROXY=true
- DEFAULT_ADMIN_EMAIL=you@example.com
- DEFAULT_ADMIN_PASSWORD=${ADMIN_PASSWORD}
- DEFAULT_ADMIN_NAME=Your Name
- DEFAULT_ADMIN_USERNAME=admin
networks:
- proxy
- internal
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.planka.rule=Host(`kanban.example.com`)"
- "traefik.http.routers.planka.entrypoints=websecure"
- "traefik.http.routers.planka.tls.certresolver=default"
- "traefik.http.services.planka.loadbalancer.server.port=1337"
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=planka
- POSTGRES_USER=planka
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U planka -d planka"]
interval: 10s
timeout: 5s
retries: 5
volumes:
planka-data:
db-data:
networks:
proxy:
external: true
internal:การตัดสินใจ 4 ประการในไฟล์นั้นควรค่าแก่การอธิบาย เพราะเป็นจุดที่ผู้คนมักเปลี่ยนแล้วต้องมาเสียใจภายหลัง
- ไม่มีบล็อก
ports:ในบริการ Planka เนื่องจาก Traefik เข้าถึงคอนเทนเนอร์ผ่านเครือข่ายproxyดังนั้นพอร์ต 1337 จึงไม่ถูกเผยแพร่ออกมาที่โฮสต์ การเผยแพร่พอร์ตดังกล่าวจะเปิดช่องทางให้ผู้อื่นเลี่ยง proxy และใบรับรองของคุณได้ loadbalancer.server.port=1337ระบุชื่อพอร์ตภายในคอนเทนเนอร์ Planka ฟังการเชื่อมต่อที่พอร์ต 1337 และตัวอย่าง upstream ทั่วไปจะเข้าถึงได้ที่พอร์ต 3000 เพราะมีการแมปพอร์ตไปยังโฮสต์ แต่ในที่นี้ไม่มีการแมปพอร์ตโฮสต์ จึงต้องระบุพอร์ตของคอนเทนเนอร์ให้ Traefik ทราบcondition: service_healthyทำงานร่วมกับการตรวจสอบสถานะ (healthcheck) ของ Postgres หากไม่มีส่วนนี้ Planka จะเริ่มทำงานก่อนที่ฐานข้อมูลจะพร้อมรับการเชื่อมต่อ ทำให้การคิวรีครั้งแรกล้มเหลวและโปรแกรมปิดตัวลง ซึ่งดูเหมือนอาการ crash loop กลไกการทำงานอธิบายไว้ใน การตรวจสอบสถานะและการจัดลำดับการเริ่มทำงานของ Compose- บริการฐานข้อมูลถูกตั้งชื่อว่า
postgresโดยเจตนา เนื่องจาก Planka 2 จะกำหนดเส้นทางคำขอขาออกผ่านตัวกรองภายใน ซึ่งรายการบล็อกเริ่มต้นคือlocalhost,postgresหากคุณเปลี่ยนชื่อบริการ คุณจะนำฐานข้อมูลของคุณออกจากรายการดังกล่าวโดยไม่รู้ตัว
ตรวจสอบว่า Compose สามารถมองเห็นข้อมูลลับของคุณก่อนที่จะเริ่มดำเนินการใดๆ:
docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'คำสั่งนี้จะแสดงไฟล์ที่มีการแทนที่ค่า .env เรียบร้อยแล้ว หากค่าว่างเปล่าแสดงว่า Compose ไม่ได้อ่านไฟล์ .env ซึ่งมักเกิดจากการที่คุณรันคำสั่งจากไดเรกทอรีอื่น
สิ่งที่ตัวแปร bootstrap สำหรับผู้ดูแลระบบทำจริง ๆ
ตั้งแต่ Planka 1.13 เป็นต้นมา ระบบจะไม่สร้างบัญชีผู้ดูแลระบบให้คุณโดยอัตโนมัติ ดังนั้นฐานข้อมูลที่เพิ่งติดตั้งใหม่จะไม่มีผู้ใช้ที่สามารถเข้าสู่ระบบได้ กลุ่มตัวแปร DEFAULT_ADMIN_* จึงเป็นหนึ่งในสองวิธีในการแก้ไขปัญหานี้
เมื่อเริ่มต้นระบบ Planka จะตรวจสอบว่ามีผู้ใช้ที่ตรงกับ DEFAULT_ADMIN_EMAIL หรือไม่ หากไม่มี ระบบจะสร้างบัญชีขึ้นมาใหม่โดยใช้รหัสผ่าน ชื่อที่แสดง และชื่อผู้ใช้ที่กำหนดไว้คู่กัน กระบวนการนี้จะเกิดขึ้นในการบูตครั้งแรกที่พบฐานข้อมูลว่างเปล่า ดังนั้นตัวแปรเหล่านี้จึงทำหน้าที่สร้างบัญชีเริ่มต้น (bootstrap) มากกว่าที่จะใช้จัดการบัญชีในระยะยาว
DEFAULT_ADMIN_EMAIL ทำหน้าที่อีกอย่างหนึ่งที่มักทำให้ผู้ใช้สับสน ในขณะที่ตัวแปรนี้ยังคงถูกตั้งค่าไว้ บัญชีที่ระบุชื่อไว้จะไม่สามารถแก้ไขหรือลบผ่านหน้าจออินเทอร์เฟซโดยใครได้เลย นี่คือกลไกป้องกันการล็อกเอาต์ และเป็นเหตุผลว่าทำไมคุณจึงไม่สามารถเปลี่ยนชื่อบัญชีหรือเปลี่ยนที่อยู่อีเมลของบัญชีนั้นใน UI ได้ หากคุณลบตัวแปรนี้ออกแล้วรีสตาร์ทระบบ บัญชีดังกล่าวจะกลายเป็นบัญชีผู้ดูแลระบบปกติที่คุณสามารถแก้ไขได้เหมือนบัญชีอื่น ๆ
บรรทัดรหัสผ่านเป็นส่วนที่ต้องระมัดระวังเป็นพิเศษ ข้อมูลใด ๆ ภายใต้ environment: สามารถอ่านได้โดยทุกคนที่สามารถรันคำสั่ง docker inspect บนคอนเทนเนอร์ได้ ดังนั้น DEFAULT_ADMIN_PASSWORD จึงไม่ควรถูกเก็บไว้ที่นั่นอย่างถาวร ให้คุณเข้าสู่ระบบ เปลี่ยนรหัสผ่านในอินเทอร์เฟซ ลบบรรทัดดังกล่าวออก แล้วรันคำสั่ง docker compose up -d อีกครั้ง
วิธีที่สะอาดกว่าคือการข้ามการใช้ตัวแปรเหล่านี้ไปเลย ให้ใส่เครื่องหมายคอมเมนต์ปิดกลุ่ม DEFAULT_ADMIN_* ทั้งหมด แล้วสร้างบัญชีผ่านการโต้ตอบแทน:
docker compose run --rm planka npm run db:create-admin-userระบบจะถามอีเมล รหัสผ่าน ชื่อที่แสดง และชื่อผู้ใช้ (ไม่บังคับ) จากนั้นจะเขียนข้อมูลผู้ใช้ลงในฐานข้อมูลโดยตรง รหัสผ่านจะไม่ถูกบันทึกไว้ในไฟล์ Compose หรือสภาพแวดล้อมของคอนเทนเนอร์ ให้ใช้วิธีนี้หากมีบุคคลอื่นที่มีสิทธิ์เข้าถึง shell ของ VPS คำสั่งนี้จะเริ่มการทำงานของ Postgres ก่อนเนื่องจาก depends_on ดังนั้นจึงสามารถใช้งานได้แม้ใน stack ที่ยังไม่เคยถูกเปิดใช้งานมาก่อน
ไม่ว่าจะใช้วิธีใด คุณยังคงต้องจัดการรหัสผ่านของ Planka ด้วยตนเอง และหากนี่เป็นชุดข้อมูลรับรองชุดที่สี่ที่ทีมของคุณต้องดูแล Planka สามารถเปลี่ยนไปใช้วิธีมอบหมายการเข้าสู่ระบบให้กับผู้ให้บริการ OIDC ได้ เช่น Authentik ที่รันเป็นเซิร์ฟเวอร์ Single Sign-On ของคุณเอง โดยให้เก็บบัญชีผู้ดูแลระบบที่สร้างผ่าน bootstrap ไว้เป็นบัญชีสำรองสำหรับกรณีฉุกเฉินในวันที่ผู้ให้บริการหลักไม่สามารถใช้งานได้
เหตุใด BASE_URL จึงทำให้การเข้าสู่ระบบล้มเหลวเมื่อไม่ตรงกับชื่อโฮสต์
BASE_URL คือที่อยู่ที่ผู้ใช้พิมพ์ลงในเบราว์เซอร์โดยตรง โดยระบุ scheme และไม่มีเครื่องหมายทับปิดท้าย สำหรับ stack นี้คือ https://kanban.example.com แอป Planka จะสร้างลิงก์และเชื่อมต่อ WebSocket โดยอ้างอิงจากค่านี้ ดังนั้นหาก BASE_URL ไม่ถูกต้อง คุณจะไม่ได้รับข้อความแจ้งเตือนข้อผิดพลาดที่ชัดเจน แต่จะพบกับหน้าเว็บที่โหลดค้างอยู่ตลอดเวลา
กรณีที่พบบ่อยคือการคัดลอกตัวอย่างจาก upstream โดยปล่อยให้ BASE_URL=http://localhost:3000 เป็นค่าเดิม แล้วเข้าใช้งานผ่าน HTTPS ด้วยโดเมนจริงของคุณ เมื่อส่งแบบฟอร์มเข้าสู่ระบบ ข้อมูลประจำตัวจะได้รับการยอมรับ แต่หน้าบอร์ดจะไม่แสดงผล หากเปิดเครื่องมือสำหรับนักพัฒนาในเบราว์เซอร์ คุณจะพบว่าคำขอไปยัง /socket.io/ ล้มเหลว เนื่องจากไคลเอนต์ได้รับคำสั่งให้เปิดการเชื่อมต่อสดไปยัง localhost:3000 ซึ่งที่อยู่ดังกล่าวไม่มีอยู่จริงบนเครื่องของคุณ
TRUST_PROXY=true คืออีกครึ่งหนึ่งของปัญหาเดียวกัน Planka ทำงานอยู่หลัง Traefik ดังนั้นทุกคำขอจะส่งมาจากที่อยู่ของพร็อกซีผ่าน HTTP ธรรมดาภายในเครือข่าย Docker หากไม่มี TRUST_PROXY แอปจะเพิกเฉยต่อ header X-Forwarded-Proto และ X-Forwarded-For ที่ Traefik ตั้งค่าไว้ ทำให้แอปเข้าใจว่าการเชื่อมต่อนั้นไม่ปลอดภัยและมองว่าไคลเอนต์ทุกรายใช้ IP address เดียวกัน เมื่อตั้งค่านี้แล้ว แอปจะอ่าน header ดังกล่าวและรับทราบ scheme ที่ตรงกับเบราว์เซอร์
Traefik สามารถทำพร็อกซี WebSockets ได้โดยไม่ต้องตั้งค่าเพิ่มเติม ซึ่งเป็นเหตุผลที่แนะนำให้ใช้ในกรณีนี้ สำหรับ nginx นั้น socket.io จำเป็นต้องมีบล็อก location ของตนเองที่ระบุ proxy_set_header Upgrade $http_upgrade และ proxy_set_header Connection "upgrade" ไม่เช่นนั้นคุณจะพบปัญหาหน้าเว็บค้างในลักษณะเดียวกันแต่เกิดจากสาเหตุอื่น
การย้ายบอร์ดไปยังชื่อโฮสต์ใหม่ในภายหลังจำเป็นต้องเปลี่ยนสองสิ่งควบคู่กัน คือค่า BASE_URL และกฎ Host() ของ Traefik หากเปลี่ยนอย่างใดอย่างหนึ่งโดยลืมอีกอย่างหนึ่ง คุณจะกลับไปพบปัญหาหน้าเว็บค้างอีกครั้ง การให้บริการ Planka ผ่าน subpath เช่น https://example.com/planka สามารถทำได้ตั้งแต่เวอร์ชัน 2.1.0 เป็นต้นไป ซึ่งปล่อยออกมาในเดือนมีนาคม 2026 สำหรับเวอร์ชันที่เก่ากว่านี้ ควรใช้ subdomain แยกต่างหากแทน
ตำแหน่งที่ Planka จัดเก็บไฟล์แนบและรูปประจำตัว
Planka 2 จัดเก็บทุกสิ่งที่ผู้ใช้อัปโหลดไว้ภายใต้ path เดียวภายในคอนเทนเนอร์ คือ /app/data ไม่ว่าจะเป็นไฟล์แนบ รูปประจำตัวผู้ใช้ หรือรูปพื้นหลังของบอร์ด ทั้งหมดจะถูกเก็บไว้ที่นี่ ในเวอร์ชัน 1 จะใช้ไดเรกทอรีแยกกัน 3 แห่ง ดังนั้นหากคัดลอกไฟล์ Compose จากบทความเก่ามาใช้ จะเป็นการ mount path ที่ไม่มีอยู่จริง และทำให้ไดเรกทอรีข้อมูลจริงไม่ได้ถูก mount ไว้
การ mount เพียงจุดเดียวนี้คือความแตกต่างระหว่างบอร์ดที่ยังคงอยู่หลังการอัปเกรดกับบอร์ดที่ทำให้คุณต้องปวดหัว หาก /app/data ไม่ได้อยู่บน volume ข้อมูลที่อัปโหลดจะไปอยู่ใน writable layer ของคอนเทนเนอร์ ซึ่ง layer นี้จะถูกทำลายเมื่อคอนเทนเนอร์ถูกสร้างใหม่ และคอนเทนเนอร์จะถูกสร้างใหม่ทุกครั้งที่คุณเปลี่ยน image tag บอร์ดจะกลับมาดูเหมือนปกติ การ์ดทุกใบยังอยู่ครบ แต่ลิงก์ไฟล์แนบทั้งหมดจะใช้งานไม่ได้ เพราะแถวในฐานข้อมูลยังคงชี้ไปยังไฟล์ที่ไม่มีอยู่แล้ว
Named volume ในไฟล์ Compose ด้านบนช่วยป้องกันปัญหานี้ได้ การใช้ bind mount ก็ทำได้เช่นกันและช่วยให้สำรองข้อมูลไฟล์ได้ง่ายขึ้นด้วยเครื่องมือทั่วไป แต่ต้องมีขั้นตอนเพิ่มเติมอีกหนึ่งอย่าง เนื่องจากกระบวนการ Node ภายในคอนเทนเนอร์รันด้วย UID 1000 ดังนั้นหากไดเรกทอรีบนโฮสต์เป็นของ root จะทำให้เกิดข้อผิดพลาดเรื่องสิทธิ์การเข้าถึงเมื่อมีการอัปโหลดครั้งแรก:
sudo chown -R 1000:1000 /opt/planka/dataข้อดีและข้อเสียระหว่างทั้งสองวิธีได้อธิบายไว้ใน bind mounts เทียบกับ named volumes
หากไฟล์แนบมีขนาดใหญ่เกินกว่าพื้นที่ดิสก์ที่คุณมี Planka สามารถเขียนไฟล์เหล่านั้นไปยัง storage ที่รองรับ S3 แทนได้ ผ่านทาง S3_ENDPOINT, S3_BUCKET และตัวแปร key ที่เกี่ยวข้อง ซึ่งสามารถชี้ไปยัง bucket ที่โฮสต์ไว้หรือ MinIO object store ที่คุณโฮสต์เอง บนเครื่องอื่น ควรตัดสินใจเรื่องนี้ก่อนที่ทีมงานจะเริ่มใช้งานบอร์ด เพราะการตั้งค่านี้จะมีผลกับไฟล์ที่อัปโหลดใหม่เท่านั้น
เริ่มการทำงานของ stack และตรวจสอบความถูกต้อง
docker compose pull
docker compose up -d
docker compose psdocker compose ps ควรแสดง postgres เป็น healthy และ planka เป็น running หาก Planka มีการรีสตาร์ทแบบวนลูป สิ่งแรกที่ต้องตรวจสอบคือการเชื่อมต่อฐานข้อมูล ไม่ใช่ตัวแอปพลิเคชัน
docker compose logs -f plankaการบูตครั้งแรกที่สมบูรณ์จะดำเนินการ database migrations จากนั้นจะรายงานว่าเซิร์ฟเวอร์กำลังรอรับการเชื่อมต่อที่พอร์ต 1337 ให้ยืนยันว่า schema ถูกสร้างขึ้นจริงโดยการสอบถาม Postgres โดยตรง แทนที่จะเชื่อเพียงแค่ log:
docker compose exec postgres psql -U planka -d planka -c '\dt'รายการตารางที่รวมถึง board และ card หมายความว่า migrations ทำงานสำเร็จ หากพบข้อความ "Did not find any relations" แสดงว่า Planka ไม่สามารถเชื่อมต่อได้ ให้เปรียบเทียบค่า DATABASE_URL กับค่า POSTGRES_USER และ POSTGRES_PASSWORD ใน .env ของคุณ
จากนั้นให้ตรวจสอบเส้นทาง (route) จากเครื่องของคุณเอง ไม่ใช่จาก VPS:
curl -I https://kanban.example.comHTTP/2 200 หมายความว่า Traefik มี certificate และสามารถเข้าถึง container ได้ หากได้รับข้อผิดพลาด 404 จาก Traefik แสดงว่า router labels ไม่ตรงกัน ซึ่งมักเกิดจากการที่ container ไม่ได้เชื่อมต่อกับเครือข่าย proxy จากนั้นให้เปิดเว็บไซต์และเข้าสู่ระบบด้วยบัญชีผู้ดูแลระบบ (admin)
สำรองข้อมูลด้วย pg_dump ก่อนการอัปเกรดเวอร์ชันทุกครั้ง
ข้อมูลของคุณถูกจัดเก็บไว้ในสองส่วนแยกกัน ดังนั้นการสำรองข้อมูลจึงต้องครอบคลุมทั้งสองส่วน ได้แก่ ฐานข้อมูล Postgres และ volume ของ planka-data ให้ทำการ dump ฐานข้อมูลในขณะที่ stack กำลังทำงานอยู่
docker compose exec -T postgres pg_dump -U planka -d planka > "planka-db-$(date +%F).sql"การใช้ -T เป็นสิ่งที่จำเป็น หากไม่มี flag นี้ Compose จะจัดสรร pseudo-terminal ให้ ซึ่งเลเยอร์ของ terminal จะเขียนทับการขึ้นบรรทัดใหม่ใน stream ส่งผลให้ไฟล์ dump ที่ได้เสียหายและไม่สามารถกู้คืนได้สำเร็จ ความล้มเหลวนี้อาจปรากฏให้เห็นในอีกหลายสัปดาห์ถัดมา ซึ่งเป็นช่วงเวลาที่เลวร้ายที่สุดในการพบปัญหา
จากนั้นให้สำรองข้อมูลส่วนอัปโหลด ขั้นแรกให้ค้นหาชื่อ volume ที่แท้จริงก่อน เนื่องจาก Compose จะเติมชื่อไดเรกทอรีโปรเจกต์นำหน้าชื่อ volume เสมอ
docker volume ls | grep planka
docker run --rm -v planka_planka-data:/data -v "$PWD":/backup alpine \
tar czf /backup/planka-files-$(date +%F).tgz -C /data .โปรเจกต์นี้ยังมาพร้อมกับ docker-backup.sh และ docker-restore.sh ใน repository และเอกสารอย่างเป็นทางการแนะนำให้ตั้งค่าเป็นงาน cron job รายวัน คุณสามารถเลือกใช้วิธีใดก็ได้ แต่สิ่งที่ยอมรับไม่ได้คือการมีไฟล์สำรองข้อมูลที่คุณไม่เคยทดสอบกู้คืนมาก่อน ดังนั้นให้ลองกู้คืนข้อมูลลงใน VPS สำรองสักครั้งเพื่อยืนยันว่าคุณสามารถล็อกอินและเปิดไฟล์แนบได้สำเร็จ รูปแบบการจัดเก็บข้อมูลแบบเดียวกันนี้พบได้ในแอปพลิเคชัน Compose ทุกตัวที่รองรับการอัปโหลด ดังนั้นหากในอนาคตคุณติดตั้ง Chatwoot บนเซิร์ฟเวอร์เดียวกับฝ่ายสนับสนุนของคุณ ขั้นตอนที่คุณสร้างขึ้นในที่นี้จะสามารถนำไปปรับใช้ต่อได้โดยเพียงแค่เปลี่ยนชื่อ volume เท่านั้น
ให้ทำการ dump ข้อมูลทันทีก่อนการเปลี่ยนเวอร์ชันทุกครั้ง ไฟล์สำรองข้อมูลจากเมื่อคืนไม่สามารถทดแทนไฟล์สำรองข้อมูลก่อนการย้ายระบบที่คุณกำลังจะดำเนินการได้
ตรึงแท็กและอ่านบันทึกประจำรุ่น (release notes)
แท็กอิมเมจทั้งสองรายการในไฟล์นั้นถูกตรึงไว้โดยเจตนา
ghcr.io/plankanban/planka:2.1.1 คือรุ่นที่ระบุไว้อย่างชัดเจน ซึ่งเป็นรุ่นปัจจุบัน ณ เดือนสิงหาคม 2026 ส่วน latest จะเปลี่ยนแปลงทุกครั้งที่มีการเผยแพร่จากต้นทาง ดังนั้นการรัน docker compose pull ตามปกติอาจทำให้เกิดการย้ายโครงสร้างฐานข้อมูล (schema migration) ในช่วงเวลาที่คุณไม่ได้เลือก โปรดอ่าน บันทึกประจำรุ่น ก่อนที่คุณจะเปลี่ยนตัวเลขดังกล่าว เพราะเป็นที่ที่อธิบายถึงการเปลี่ยนแปลงที่ส่งผลกระทบต่อระบบ (breaking changes) และการแก้ไขด้านความปลอดภัย รุ่น 2.0.3 ถูกเผยแพร่ในฐานะรุ่นเพื่อความปลอดภัย ซึ่งเป็นข้อมูลประเภทที่คุณควรทราบล่วงหน้าแทนที่จะได้รับผลกระทบโดยไม่ตั้งใจ การตรึงเวอร์ชันทำได้ง่ายในกรณีนี้เนื่องจากต้นทางมีการเผยแพร่ภาพอิมเมจอยู่แล้ว และในกรณีที่โปรเจกต์ใดไม่มีการเผยแพร่ คุณควรใช้ความระมัดระวังในระดับเดียวกันพร้อมขั้นตอนเพิ่มเติม เช่นเดียวกับ การ build openGym บนเครื่องจาก git tag ที่ checkout มา
postgres:16-alpine ถูกตรึงไว้ที่เวอร์ชันหลัก (major version) ด้วยเหตุผลที่สำคัญกว่า Postgres เขียนไดเรกทอรีข้อมูลในรูปแบบที่ผูกติดกับเวอร์ชันหลัก และเซิร์ฟเวอร์จะปฏิเสธการเปิดไดเรกทอรีที่เขียนโดยเวอร์ชันอื่น หากคุณเขียน postgres:latest แล้วปล่อยให้แท็กเปลี่ยนเป็น 17 คอนเทนเนอร์จะไม่เริ่มทำงาน:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.ไม่มีข้อมูลใดสูญหาย และไม่มีสิ่งใดได้รับการแก้ไขด้วยการรีสตาร์ท การย้ายไปยัง Postgres เวอร์ชันหลักใหม่หมายถึงการทำ dump ข้อมูลจากเวอร์ชันเก่าและ restore ลงในไดเรกทอรีข้อมูลใหม่ที่ว่างเปล่า นั่นเป็นงานที่ต้องวางแผนโดยหยุดการทำงานของ stack ไม่ใช่ผลกระทบข้างเคียงจากการดึงอิมเมจ (image pull)
หากคุณกำลังย้ายการติดตั้ง Planka 1.x ที่มีอยู่เดิมแทนที่จะเริ่มใหม่ทั้งหมด การอัปเกรดนั้นมีขั้นตอนที่ระบุไว้ใน เอกสารประกอบของโปรเจกต์ และไม่มีทางย้อนกลับไปใช้เวอร์ชัน 1 ได้หากไม่มีการสำรองข้อมูลไว้ล่วงหน้า
รูปแบบความล้มเหลวและข้อความที่คุณจะพบ
Planka รีสตาร์ทวนลูปและ log ระบุถึงฐานข้อมูล ข้อมูลรับรองใน DATABASE_URL ไม่ตรงกับสภาพแวดล้อมของ Postgres โปรดทราบว่า POSTGRES_PASSWORD จะถูกนำไปใช้เฉพาะตอนที่ไดเรกทอรีข้อมูลถูกเริ่มต้นใช้งานครั้งแรกเท่านั้น ดังนั้นการแก้ไขตัวแปรหลังจากบูตครั้งแรกที่ไม่สำเร็จจะไม่ส่งผลใดๆ คุณต้องลบ volume db-data ออกแล้วเริ่มใหม่
เข้าสู่ระบบสำเร็จแต่บอร์ดไม่โหลด BASE_URL ไม่ตรงกับที่อยู่ในแถบเบราว์เซอร์ หรือ TRUST_PROXY หายไป คอนโซลของเบราว์เซอร์จะแสดงคำขอที่ล้มเหลวไปยัง /socket.io/
การอัปโหลดล้มเหลวในขณะที่ส่วนอื่นทำงานปกติ เกิดจาก bind mount ที่มี root เป็นเจ้าของ ให้รัน sudo chown -R 1000:1000 บนไดเรกทอรีของโฮสต์แล้วรีสตาร์ทคอนเทนเนอร์
ไฟล์แนบหายไปหลังจากการอัปเกรด /app/data ไม่ได้อยู่บน volume ไฟล์จึงค้างอยู่ในเลเยอร์ของคอนเทนเนอร์ซึ่งถูกแทนที่ไปแล้วตอนอัปเกรด ให้กู้คืนไฟล์จากข้อมูลสำรอง จากนั้นเพิ่ม volume ก่อนที่คุณจะแตะ image tag อีกครั้ง
Traefik ส่งคืนค่า 404 คอนเทนเนอร์ไม่ได้อยู่บนเครือข่าย proxy หรือกฎ Host() ไม่ตรงกับระเบียน DNS ของคุณ docker compose config จะแสดง labels หลังจากแทนที่ค่าแล้ว ซึ่งเป็นจุดที่สามารถมองเห็นคำผิดได้
การแจ้งเตือนหรือ webhooks ไม่ทำงาน Planka 2 ส่งคำขอ HTTP ขาออกผ่านตัวกรองภายใน และรายการบล็อกเริ่มต้นครอบคลุมถึง localhost และ postgres ดังนั้น webhook ที่มุ่งเป้าไปยังคอนเทนเนอร์อื่นบนโฮสต์เดียวกันอาจถูกบล็อกโดยการออกแบบ ให้ปรับแต่ง OUTGOING_ALLOWED_HOSTS แทนการลบตัวกรองออก
เมื่อระบบทำงานแล้ว ภาระในการดูแลรักษาจะน้อยมาก ให้ติดตามบันทึกการปล่อยซอฟต์แวร์ (release notes) และสำรองข้อมูลฐานข้อมูลก่อนการอัปเกรดทุกครั้ง การรีบูตจะทำให้ stack กลับมาทำงานได้เองเนื่องจาก restart: unless-stopped ตราบใดที่บริการ Docker ถูกเปิดใช้งานตอนบูต และ Compose stacks ที่กลับมาทำงานหลังรีบูต จะครอบคลุมกรณีที่ระบบไม่กลับมาทำงานเอง
FAQ
ทำไม Planka ถึงโหลดค้างหลังจากที่ฉันเข้าสู่ระบบ?
ข้อมูลประจำตัวได้รับการยอมรับแต่การเชื่อมต่อแบบ live ไม่สำเร็จ Planka สร้าง WebSocket URL จาก BASE_URL ดังนั้นหากตัวแปรดังกล่าวยังคงเป็น http://localhost:3000 ในขณะที่คุณเข้าใช้งานเว็บไซต์ผ่าน https://kanban.example.com เบราว์เซอร์จะพยายามเปิด socket ไปยังที่อยู่ที่ไม่มีอยู่จริงบนเครื่องของคุณ คอนโซลสำหรับนักพัฒนาจะแสดงคำขอที่ล้มเหลวไปยัง /socket.io/ ให้ตั้งค่า BASE_URL เป็นที่อยู่สาธารณะที่ถูกต้องโดยไม่มีเครื่องหมายทับปิดท้าย และเพิ่ม TRUST_PROXY=true เพื่อให้แอปยอมรับ header X-Forwarded-Proto จาก reverse proxy ของคุณ จากนั้นรันคำสั่ง docker compose up -d
ฉันจะสร้างผู้ดูแลระบบคนแรกของ Planka ได้อย่างไร?
ตั้งแต่เวอร์ชัน 1.13 เป็นต้นมา จะไม่มีการสร้างผู้ดูแลระบบโดยอัตโนมัติ คุณสามารถตั้งค่า DEFAULT_ADMIN_EMAIL พร้อมกับตัวแปรรหัสผ่าน ชื่อ และชื่อผู้ใช้ที่ตรงกันแล้วเริ่ม stack หรือรันคำสั่ง docker compose run --rm planka npm run db:create-admin-user แล้วตอบคำถามตามที่ระบบแจ้ง คำสั่งแบบโต้ตอบมีความปลอดภัยมากกว่าบนเซิร์ฟเวอร์ที่ใช้งานร่วมกัน เนื่องจากรหัสผ่านจะไม่ถูกส่งเข้าไปในสภาพแวดล้อมของ container ที่ซึ่ง docker inspect สามารถอ่านได้ การคงค่า DEFAULT_ADMIN_EMAIL ไว้หลังจากนั้นจะล็อกบัญชีดังกล่าวไม่ให้แก้ไขหรือลบผ่านหน้าอินเทอร์เฟซได้
Planka จัดเก็บไฟล์แนบและรูปโปรไฟล์ไว้ที่ไหน?
ไฟล์ที่อัปโหลดทั้งหมดจะอยู่ใน /app/data ภายใน container ใน Planka 2 ซึ่งรวมถึงไฟล์แนบ รูปโปรไฟล์ผู้ใช้ และภาพพื้นหลังของบอร์ด ให้ mount path ดังกล่าวไว้บน named volume หากไม่ได้ mount ไว้ ไฟล์จะอยู่ในชั้นที่เขียนได้ (writable layer) ของ container และจะถูกลบเมื่อมีการสร้าง container ใหม่ ซึ่งจะเกิดขึ้นทุกครั้งที่มีการอัปเกรด image การใช้ bind mount ก็สามารถทำได้เช่นกัน แต่เนื่องจากกระบวนการ Node ทำงานด้วย UID 1000 คุณจึงต้องรันคำสั่ง sudo chown -R 1000:1000 บนไดเรกทอรีของโฮสต์ มิฉะนั้นการอัปโหลดจะล้มเหลวเนื่องจากข้อผิดพลาดด้านสิทธิ์ (permission error)
การ self-host Planka ต้องใช้ RAM เท่าไหร่?
ทางโครงการไม่ได้กำหนดสเปกฮาร์ดแวร์ขั้นต่ำไว้ ตัวเลข 2 vCPU และ 4 GB ที่ปรากฏบนหน้าเว็บของผู้ให้บริการเป็นค่าเริ่มต้นของผู้ให้บริการมากกว่าที่จะเป็นค่าที่วัดได้จริง และถือว่าเพียงพอมากสำหรับบอร์ดขนาดเล็ก ภาระงานทั้งหมดมีเพียงกระบวนการ Node หนึ่งรายการและกระบวนการ Postgres หนึ่งรายการ ดังนั้นแผน 1 vCPU และ 2 GB จึงเพียงพอสำหรับทีมขนาด 2 ถึง 5 คน ให้รันคำสั่ง docker stats --no-stream หลังจากใช้งานตามปกติครบหนึ่งสัปดาห์แล้วจึงปรับขนาดตามตัวเลขการใช้งานจริงของคุณ ให้เฝ้าระวังพื้นที่ดิสก์มากกว่าหน่วยความจำ เนื่องจากไฟล์แนบคือส่วนที่จะเพิ่มขนาดขึ้นเรื่อยๆ
ฉันจะอัปเกรด Planka โดยไม่ให้ข้อมูลสูญหายได้อย่างไร?
ให้สำรองข้อมูลฐานข้อมูล (dump) และเก็บถาวร (archive) volume ของไฟล์ที่อัปโหลดทันทีก่อนการอัปเกรด ไม่ใช่ใช้ตารางเวลาของเมื่อคืนนี้ ให้ใช้คำสั่ง docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql โดยคงค่า -T ไว้เพื่อให้ pseudo-terminal ไม่ทำให้ผลลัพธ์ที่ถูก redirect เสียหาย อ่านบันทึกการเปลี่ยนแปลง (release notes) สำหรับทุกเวอร์ชันที่คุณข้ามไป เปลี่ยน image tag ให้เป็นเวอร์ชันที่ระบุแทนการใช้ latest จากนั้นรันคำสั่ง docker compose pull และ docker compose up -d พร้อมกับเฝ้าดู log เพื่อตรวจสอบการ migration ให้คงค่า tag ของ Postgres ไว้ที่เวอร์ชันหลักเดิม เพราะเซิร์ฟเวอร์จะปฏิเสธการเปิดไดเรกทอรีข้อมูลที่ถูกเขียนโดยเวอร์ชันหลักที่ต่างกัน