วิธีติดตั้ง Nextcloud บน VPS ด้วย Docker Compose
เรียนรู้วิธีรัน Nextcloud บน VPS ด้วย Docker Compose พร้อม Postgres, Redis และ TLS เพื่อความปลอดภัย รวมถึงขั้นตอนการสำรองข้อมูลและอัปเกรดที่ช่วยให้ข้อมูลของคุณปลอดภัยและกู้คืนได้จริง
สิ่งที่คุณกำลังสร้างจริง ๆ
คู่มือนี้จะแนะนำการรัน Nextcloud บน VPS ด้วย Docker Compose โดยมี Let's Encrypt TLS ทำหน้าที่เป็นหน้าด่าน และตั้งค่าระบบสำรองข้อมูลที่สามารถกู้คืนได้จริง เราจะใช้คอนเทนเนอร์ 4 ตัวและพร็อกซีหนึ่งตัว ได้แก่ อิมเมจ nextcloud อย่างเป็นทางการที่ฟังคำสั่งบน loopback, Postgres ที่เก็บข้อมูล metadata ของไฟล์ทั้งหมด, Redis ที่ทำหน้าที่จัดการ file locks, อิมเมจ Nextcloud ตัวที่สองที่รันเฉพาะ cron loop และ nginx บนโฮสต์ที่ทำ TLS termination อยู่หน้าบริการทั้งหมด การติดตั้งใช้เวลา 20 นาที ซึ่งไม่ใช่ส่วนที่สำคัญที่สุด การตัดสินใจสองอย่างในชั่วโมงแรกจะเป็นตัวกำหนดว่าคุณจะยังคงมีไฟล์ของคุณอยู่หรือไม่ในอีกหนึ่งปีข้างหน้า นั่นคือการใช้ฐานข้อมูลจริงแทน SQLite และการสำรองข้อมูลที่จัดเก็บไดเรกทอรีข้อมูล ฐานข้อมูล และ config.php ไว้เป็นชุดข้อมูลที่สอดคล้องกัน
คู่มือนี้ตั้งสมมติฐานว่าคุณใช้ Ubuntu 24.04 LTS หรือ Debian 13, ติดตั้ง Docker Engine พร้อม Compose v2 plugin จาก repository ของ Docker โดยตรง และมีระเบียน DNS A (รวมถึง AAAA หากคุณมี IPv6) ที่ชี้ cloud.example.com มายัง VPS ของคุณเรียบร้อยแล้ว ทั้งหมดนี้จำเป็นต้องใช้เซิร์ฟเวอร์ที่คุณควบคุมเอง เนื่องจากคุณไม่สามารถทำ TLS termination และดึงข้อมูลฐานข้อมูล (dump) บนบริการ SaaS ของผู้อื่นได้
การประเมินขนาด: สิ่งที่ใช้หน่วยความจำจริง
การใช้หน่วยความจำของ Nextcloud ขึ้นอยู่กับปัจจัยหลัก 3 ประการ ซึ่งไม่มีข้อใดที่เป็นตัว "Nextcloud" โดยตรง
PHP workers: อิมเมจ -apache จะให้บริการคำขอที่เข้ามาพร้อมกันแต่ละรายการผ่านกระบวนการ worker ที่รัน PHP interpreter อยู่ โดย worker แต่ละตัวอาจขยายขนาดได้สูงสุดถึง PHP_MEMORY_LIMIT ก่อนที่ PHP จะยุติคำขอนั้น หน่วยความจำที่ถูกใช้งานจริงในกรณีเลวร้ายที่สุดจะอยู่ที่ประมาณ จำนวนคำขอที่เข้ามาพร้อมกัน × ขีดจำกัดหน่วยความจำ และไคลเอนต์สำหรับซิงค์บนเดสก์ท็อปจะเปิดการเชื่อมต่อแบบขนานหลายรายการต่อผู้ใช้หนึ่งคน ดังนั้นจำนวนการทำงานพร้อมกัน (concurrency) ไม่ใช่จำนวนผู้ใช้ที่เป็นตัวกำหนดเพดานการใช้งาน
ฐานข้อมูล: Postgres จะทำการ fork กระบวนการ backend ต่อการเชื่อมต่อหนึ่งรายการและเก็บ shared buffers ไว้ในหน่วยความจำ ชุดข้อมูลที่ใช้งานจริง (working set) จะแปรผันตามจำนวน ไฟล์ ไม่ใช่จำนวนไบต์ โดย oc_filecache จะเก็บข้อมูลหนึ่งแถวต่อหนึ่งไฟล์ต่อผู้ใช้ ไฟล์ขนาดเล็กจำนวนหนึ่งแสนไฟล์จะสร้างภาระให้ฐานข้อมูลมากกว่าไฟล์ขนาดใหญ่จำนวนหนึ่งร้อยไฟล์
การสร้างภาพตัวอย่าง (Preview generation): การสร้างภาพขนาดย่อจะถอดรหัสภาพต้นฉบับลงในหน่วยความจำที่ความละเอียดเต็มรูปแบบ ส่วนการสร้างภาพตัวอย่างวิดีโอจะเรียกใช้ ffmpeg การรัน occ preview:generate-all จะทำให้เกิดการใช้หน่วยความจำพุ่งสูงขึ้นซ้ำๆ อย่างต่อเนื่อง และเป็นสาเหตุที่พบบ่อยที่สุดที่ทำให้ VPS ขนาดเล็กถูก OOM killer สั่งยุติการทำงาน
Redis มีการใช้ทรัพยากรที่ค่อนข้างต่ำ ส่วนบริการอื่นๆ ที่คุณติดตั้งเพิ่มในภายหลัง เช่น Collabora, full-text search หรือโปรแกรมสแกนไวรัส จะเป็นบริการที่รันแยกต่างหากและใช้หน่วยความจำของตัวเอง ซึ่งคุณควรนำมาคำนวณในแผนการประเมินขนาดก่อนที่จะเปิดใช้งาน
หากคุณมี RAM จำกัด ให้ปรับค่าดังนี้: ลดค่า PHP_MEMORY_LIMIT, จำกัดค่า preview_max_x / preview_max_y / preview_max_filesize_image, ปรับแต่ง enabledPreviewProviders ให้เหลือเฉพาะรูปแบบไฟล์ที่คุณเปิดดูจริง และตั้งค่า trashbin_retention_obligation กับ versions_retention_obligation เพื่อไม่ให้ไดเรกทอรีข้อมูลขยายขนาดขึ้นจนใหญ่กว่าขนาดไฟล์จริงของคุณหลายเท่าโดยไม่รู้ตัว นอกจากนี้ควรเพิ่ม swap file แม้ว่า swap จะทำงานช้า แต่การถูก OOM kill ในระหว่างการอัปเกรดนั้นส่งผลเสียมากกว่า
เหตุใด SQLite จึงมีปัญหา
Nextcloud มาพร้อมกับการรองรับ SQLite และอิมเมจอย่างเป็นทางการก็พร้อมใช้งานมันทันที แต่คุณไม่ควรใช้ SQLite จะทำการจัดลำดับการเขียนด้วยการล็อกฐานข้อมูลทั้งไฟล์ ซึ่งอนุญาตให้เขียนได้เพียงหนึ่งรายการต่อครั้งสำหรับทั้งไฟล์ Nextcloud มีการเขียนข้อมูลอยู่ตลอดเวลา ไม่ว่าจะเป็นการล็อกไฟล์, แถวข้อมูลกิจกรรม, รายการแคช, สถานะงาน และไคลเอนต์เดสก์ท็อปเพียงเครื่องเดียวที่ซิงค์โครงสร้างไดเรกทอรีก็ส่งคำขอแบบขนานจำนวนมาก ภายใต้รูปแบบการทำงานนี้ คุณจะพบกับ SQLSTATE[HY000]: General error: 5 database is locked และข้อผิดพลาด HTTP 500 ซึ่งความล้มเหลวจะปรากฏขึ้นทันทีเมื่ออินสแตนซ์เริ่มถูกใช้งานจริง
การย้ายฐานข้อมูลในภายหลังสามารถทำได้ด้วย occ db:convert-type แต่เป็นการย้ายข้อมูลที่ใช้เวลานานและต้องทำจนเสร็จสิ้นในคราวเดียวบนชุดข้อมูลที่กำลังใช้งานอยู่ ดังนั้นควรเริ่มต้นด้วย Postgres หรือ MariaDB ตั้งแต่แรก
ไฟล์ Compose
ให้วางเนื้อหานี้ไว้ใน /srv/nextcloud/compose.yaml โดยเก็บค่าความลับไว้ในไฟล์ .env ที่อยู่ในระดับเดียวกันและตั้งค่าโหมดเป็น 600
services:
db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- db:/var/lib/postgresql/data
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${DB_PASSWORD}
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
app:
image: nextcloud:35-apache
restart: unless-stopped
depends_on: [db, redis]
ports:
- "127.0.0.1:8080:80"
volumes:
- html:/var/www/html
- /srv/nextcloud/data:/var/www/html/data
environment:
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
TRUSTED_PROXIES: 172.16.0.0/12
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://cloud.example.com
APACHE_DISABLE_REWRITE_IP: "1"
PHP_MEMORY_LIMIT: 512M
PHP_UPLOAD_LIMIT: 10G
cron:
image: nextcloud:35-apache
restart: unless-stopped
entrypoint: /cron.sh
depends_on: [db, redis]
volumes:
- html:/var/www/html
- /srv/nextcloud/data:/var/www/html/data
volumes:
db:
html:ให้ระบุเวอร์ชันหลัก (major tag) ให้ชัดเจนและตรวจสอบเวอร์ชันปัจจุบันบน Docker Hub ก่อนที่จะคัดลอก 35 ไปใช้งาน ณ วันที่ 21 ตุลาคม 2026 Nextcloud ยังคงสนับสนุนเวอร์ชัน 33, 34 และ 35 ส่วนเวอร์ชัน 31 ได้สิ้นสุดระยะเวลาสนับสนุน (end of life) ไปเมื่อเดือนกุมภาพันธ์ 2026 และเวอร์ชัน 32 ในเดือนกันยายน 2026 ดังนั้นการคัดลอก tag จากคู่มือเก่าจะทำให้คุณติดตั้งซอฟต์แวร์รุ่นที่ไม่ได้รับการแก้ไขข้อผิดพลาดอีกต่อไป การใช้ latest จะทำให้เกิดการอัปเกรดข้ามเวอร์ชันหลักโดยอัตโนมัติในอนาคตเมื่อมีการปล่อย docker compose pull ใหม่ ซึ่ง Nextcloud ไม่รองรับการดำเนินการดังกล่าว
ไดเรกทอรีข้อมูลถูกกำหนดให้เป็น bind mount แทนที่จะเป็น named volume โดยมีจุดประสงค์คือ การมี path ที่เครื่องมือสำรองข้อมูลสามารถเข้าถึงได้โดยตรงนั้นมีประโยชน์มากกว่าความสะอาดของระบบ ให้สร้างไดเรกทอรีดังกล่าวด้วย UID www-data ตามที่ image กำหนด และตั้งค่าสิทธิ์การเข้าถึงตามที่ Nextcloud ต้องการ:
sudo mkdir -p /srv/nextcloud/data
sudo chown -R 33:33 /srv/nextcloud/data
sudo chmod 0770 /srv/nextcloud/dataโปรดสังเกตการเปิดพอร์ต: 127.0.0.1:8080:80 Docker จะเปิดพอร์ตโดยการเขียนกฎ DNAT ซึ่งจะถูกประมวลผลก่อนที่ chain INPUT ของ ufw จะได้รับ packet ดังนั้นการใช้ 8080:80 เปล่าๆ จะทำให้ Nextcloud ที่ไม่มีการเข้ารหัสถูกเปิดเผยต่อสาธารณะโดยไม่สนใจกฎของ ufw การผูกพอร์ตไว้กับ loopback จะช่วยป้องกันไม่ให้เข้าถึงได้จากอินเทอร์เฟซสาธารณะ จากนั้น firewall จะอนุญาตให้เข้าถึงได้เฉพาะผ่าน proxy เท่านั้น และหากคุณไม่ต้องการเปิดพอร์ต SSH ให้กับอินเทอร์เน็ตสาธารณะ การเข้าถึง VPS ผ่าน WireGuard VPN ที่โฮสต์เอง จะช่วยให้คุณสามารถปิดพอร์ต 22 จากกฎสาธารณะได้ทั้งหมด:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableเริ่มการทำงานด้วย docker compose up -d จากนั้นให้เฝ้าดู docker compose logs -f app การบูตครั้งแรกจะเป็นการคัดลอกโครงสร้างแอปพลิเคชันทั้งหมดลงใน volume และรันตัวติดตั้ง container จะยังไม่ตอบสนองใดๆ จนกว่ากระบวนการดังกล่าวจะเสร็จสิ้น
TLS และ reverse proxy
ติดตั้ง nginx และ certbot จาก repository ของ distribution สร้าง server block สำหรับพอร์ต 80 พร้อมด้วย server_name ที่ถูกต้อง จากนั้นให้ certbot ทำการเขียนทับ กลไกของ HTTP-01 challenge, ตัวตั้งเวลาต่ออายุ และรูปแบบความล้มเหลวได้อธิบายไว้อย่างละเอียดใน การออกใบรับรอง Let's Encrypt ด้วย certbot และ nginx บน Ubuntu 24.04:
sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.comCertbot จะเพิ่มบรรทัด ssl_certificate และการ redirect จาก :80 ไปยัง :443 พร้อมทั้งติดตั้ง systemd timer เพื่อต่ออายุใบรับรองที่มีอายุ 90 วัน ตรวจสอบว่า timer ดังกล่าวมีอยู่จริงด้วย systemctl list-timers | grep certbot เพราะหากตัวตั้งเวลาต่ออายุไม่ถูกเปิดใช้งาน ใบรับรองนั้นก็เปรียบเสมือนระเบิดเวลา 90 วัน
ส่วนของ proxy block:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name cloud.example.com;
# certbot manages ssl_certificate / ssl_certificate_key here
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
client_max_body_size 10G;
client_body_timeout 300s;
location = /.well-known/carddav { return 301 /remote.php/dav; }
location = /.well-known/caldav { return 301 /remote.php/dav; }
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_request_buffering off;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}สำหรับ nginx เวอร์ชัน 1.25.1 ขึ้นไป ให้เพิ่ม http2 on; คำสั่งนี้ไม่มีในเวอร์ชัน 1.25.0 และ nginx จะไม่เริ่มทำงานหากพบคำสั่งดังกล่าว Ubuntu 24.04 มาพร้อมกับ build ที่เก่ากว่าซึ่งใช้ listen 443 ssl http2; แทน คุณสามารถใช้ nginx -t เพื่อตรวจสอบว่า build ของคุณรองรับคำสั่งใด
หาก client_max_body_size มีขนาดเล็กเกินไป nginx จะปฏิเสธการอัปโหลดไฟล์ขนาดใหญ่ทันทีด้วย error 413 หากค่า read timeout สั้นเกินไป การอัปโหลดจะหยุดชะงักกลางคัน ค่าที่ระบุไว้ข้างต้นจะช่วยป้องกันปัญหาทั้งสองอย่าง ส่วน proxy_request_buffering off จะช่วยให้การอัปโหลดส่งผ่านไปยังปลายทางโดยตรง แทนที่จะเขียนไฟล์ทั้งหมดลงในดิสก์ของ proxy ก่อน
nginx บนโฮสต์เป็นวิธีที่ง่ายที่สุดสำหรับแอปพลิเคชันเดียว หาก Nextcloud ต้องใช้งานร่วมกับ container อื่นบน VPS เดียวกัน การใช้ Traefik ในฐานะ Docker Compose reverse proxy สำหรับหลายแอปพลิเคชัน จะช่วยย้ายการจัดการ routing และการออกใบรับรองไปไว้ใน container labels โดยที่ข้อกำหนดเรื่อง client_max_body_size และค่า timeout ต่างๆ จะยังคงมีผลในรูปแบบของ middleware และการตั้งค่า transport
trusted_proxies และ overwriteprotocol
นี่คือจุดที่การติดตั้ง Nextcloud แบบ self-hosted ส่วนใหญ่ทำผิดพลาด และอาการที่เกิดขึ้นมักดูไม่เกี่ยวข้องกับสาเหตุที่แท้จริง
X-Forwarded-Proto: https จะทำงานก็ต่อเมื่อคำขอมาจากที่อยู่ที่ระบุไว้ใน trusted_proxies เท่านั้น หากไม่เป็นไปตามเงื่อนไข Nextcloud จะเข้าใจว่าคำขอนั้นเป็น HTTP ธรรมดาและสร้าง URL ที่เป็น http:// ออกมา จากนั้น proxy จะสั่ง redirect ไปยัง HTTPS; เบราว์เซอร์จะทำตาม; แล้ว Nextcloud ก็จะสร้าง http:// ออกมาอีกครั้ง ซึ่งนั่นคือปัญหา redirect loop ส่วน OVERWRITEPROTOCOL: https จะเป็นการบังคับใช้ scheme โดยไม่สนใจเงื่อนไขอื่น
กับดักใน TRUSTED_PROXIES คือที่อยู่ที่ Nextcloud มองเห็นนั้น ไม่ใช่ 127.0.0.1 เนื่องจาก nginx ทำงานอยู่บน host และเชื่อมต่อผ่านพอร์ตที่เปิดไว้ container จึงมองเห็นเป็น Docker bridge gateway ซึ่งอยู่ในช่วง 172.x ให้ค้นหา subnet ที่แท้จริงด้วยคำสั่งนี้:
docker network inspect nextcloud_default \
-f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'ให้นำค่า CIDR นั้น (หรือค่า 172.16.0.0/12 ที่ครอบคลุม) ไปใส่ใน TRUSTED_PROXIES หากตั้งค่ากว้างเกินไป ไคลเอนต์ใดๆ ก็อาจปลอมแปลง X-Forwarded-For ได้ แต่หากตั้งค่าผิดพลาด การล็อกอินทุกครั้งจะดูเหมือนมาจากที่อยู่ของ gateway ทั้งหมด ทำให้ระบบป้องกัน brute-force บล็อกทั้งอินสแตนซ์ของคุณในคราวเดียว และหน้า admin overview จะแสดงข้อความเตือนว่า "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy."
OVERWRITECLIURL มีความสำคัญสำหรับ cron container เนื่องจากไม่มีคำขอขาเข้าให้ระบบใช้ระบุ hostname ได้ หากไม่มีการตั้งค่านี้ งานเบื้องหลัง (background jobs) จะสร้างลิงก์ไปยัง localhost และการแจ้งเตือนทางอีเมลจะส่ง URL ที่ใช้งานไม่ได้ไปให้ผู้ใช้
งานเบื้องหลัง: ใช้ cron แทน AJAX
ตัวรันงานเริ่มต้นของ Nextcloud คือ AJAX ซึ่งงานจะถูกประมวลผลเมื่อมีผู้ใช้งานโหลดหน้าเว็บ เนื่องจากไม่มีใครใช้งานระบบในช่วงเวลา 04:00 น. งานต่างๆ เช่น การลบไฟล์ขยะ, การล้างเวอร์ชันเก่า, การสร้างรูปตัวอย่าง (previews) และการลองเชื่อมต่อ federated ใหม่จึงหยุดชะงัก ผลกระทบแรกที่พบคือขนาดของ data directory ที่เพิ่มขึ้นอย่างต่อเนื่อง บริการ cron ด้านบนจะรันลูป /cron.sh อย่างเป็นทางการโดยใช้ volume เดียวกัน คุณต้องตั้งค่าให้ Nextcloud รับทราบการทำงานนี้:
docker compose exec -u www-data app php occ background:cronทุกคำสั่ง occ จะมีรูปแบบดังนี้: docker compose exec -u www-data app php occ <command> การสร้าง alias สำหรับคำสั่งนี้เป็นสิ่งที่ควรทำ
การสำรองข้อมูล: ต้องครบทั้งสามส่วน มิเช่นนั้นถือว่าไม่มี
การสำรองข้อมูลเฉพาะไฟล์ระบบจะทำให้กู้คืนได้เพียงอินสแตนซ์ที่ใช้งานไม่ได้ ไดเรกทอรีข้อมูลเก็บข้อมูลดิบ (bytes) ส่วน Postgres เก็บแคชไฟล์, ข้อมูลการแชร์, ผู้ใช้ และสถานะของแอปพลิเคชัน ในขณะที่ config.php เก็บข้อมูลรับรองฐานข้อมูล, ID ของอินสแตนซ์ และค่า password salt หากกู้คืนไฟล์โดยไม่มีฐานข้อมูล Nextcloud จะมองไม่เห็นไฟล์เหล่านั้น หากกู้คืนฐานข้อมูลโดยไม่มี config.php ระบบจะไม่สามารถเปิดฐานข้อมูลได้ และหากกู้คืนฐานข้อมูลเก่าทับไดเรกทอรีข้อมูลที่ใหม่กว่า จะทำให้ข้อมูลการแชร์ชี้ไปยังไฟล์ที่ถูกย้ายตำแหน่งไปแล้ว
ให้สำรองข้อมูลทั้งสามส่วนจากอินสแตนซ์ที่หยุดการทำงานชั่วคราว (quiesced instance):
#!/usr/bin/env bash
set -euo pipefail
cd /srv/nextcloud
DEST="/var/backups/nextcloud/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DEST"
occ() { docker compose exec -T -u www-data app php occ "$@"; }
occ maintenance:mode --on
trap 'occ maintenance:mode --off' EXIT
docker compose exec -T db \
pg_dump -U nextcloud --clean --if-exists nextcloud | gzip > "$DEST/db.sql.gz"
docker compose exec -T app \
tar -C /var/www/html -cf - config custom_apps themes > "$DEST/app.tar"
rsync -a --delete /srv/nextcloud/data/ /var/backups/nextcloud/data/โหมดบำรุงรักษา (Maintenance mode) คือสิ่งที่ทำให้การดัมพ์ฐานข้อมูลและการคัดลอกไฟล์มีความสอดคล้องกัน หากข้ามขั้นตอนนี้ คุณอาจได้ฐานข้อมูลที่อ้างอิงถึงไฟล์ที่ rsync ยังคัดลอกไปไม่ถึง โปรดทราบว่าสคริปต์นี้จะเก็บไฟล์ดัมพ์ฐานข้อมูลที่มีการประทับเวลาไว้ แต่จะเก็บสำเนาไดเรกทอรีข้อมูลแบบหมุนเวียนเพียงชุดเดียวเท่านั้น โดย rsync --delete จะเขียนทับข้อมูลเดิมทุกครั้งที่รัน ดังนั้นจะมีเพียงไฟล์ดัมพ์ล่าสุดเท่านั้นที่ใช้คู่กับสำเนาไฟล์ได้
จากนั้นให้นำข้อมูลออกจากเซิร์ฟเวอร์ การสำรองข้อมูลที่เก็บไว้บน VPS เครื่องเดียวกับที่สำรองนั้นถือเป็นเพียงสำเนา ไม่ใช่การสำรองข้อมูล การใช้ restic ร่วมกับ object storage หรือโฮสต์เครื่องที่สองเป็นวิธีปฏิบัติมาตรฐาน และฟีเจอร์การทำ deduplication ของเครื่องมือนี้จัดการกับไดเรกทอรีข้อมูลได้ดีกว่าการทำ tarball รายคืน การตั้งค่าทั้งหมดตั้งแต่การเริ่มต้น repository, การตั้งเวลาทำงานรายคืน ไปจนถึงการฝึกซ้อมกู้คืนข้อมูล อยู่ใน การสำรองข้อมูล VPS ไปยังภายนอกด้วย restic
การกู้คืนไม่ใช่แค่การทำย้อนกลับขั้นตอนเดิม สแต็กที่เพิ่งเริ่มทำงานใหม่จะรันตัวติดตั้งและเขียนไฟล์ config.php ใหม่ทั้งหมด รวมถึงสร้าง ID อินสแตนซ์และ password salt ใหม่ การนำเข้าไฟล์ดัมพ์ทับลงบนตัวตนใหม่นี้จะทำให้ session และโทเค็นการแชร์ใช้งานไม่ได้ คุณต้องนำตัวตนเดิมกลับมาก่อนตามลำดับนี้:
docker compose up -d && docker compose stop app cron # create the volumes, then halt the app
sudo rsync -a --delete /var/backups/nextcloud/data/ /srv/nextcloud/data/
docker compose run --rm -T --entrypoint "" app \
tar -C /var/www/html -xf - < app.tar # the original config.php returns
gunzip -c db.sql.gz | docker compose exec -T db psql -U nextcloud -d nextcloud
docker compose start app cron
docker compose exec -T -u www-data app php occ maintenance:mode --off
docker compose exec -T -u www-data app php occ files:scan --allfiles:scan จะทำการประสานแคชไฟล์ให้ตรงกับข้อมูลที่มีอยู่จริงบนดิสก์ ให้ทดลองทำขั้นตอนนี้หนึ่งครั้งบน VPS สำรองก่อนที่จะถึงเวลาจำเป็นต้องใช้งานจริง การแยกส่วนระหว่างข้อมูลบนดิสก์และข้อมูลเมตาใน Postgres เป็นหลักการที่ใช้กับแอปพลิเคชันอื่นที่มีโครงสร้างลักษณะเดียวกัน ซึ่งเป็นเหตุผลว่าทำไม การสำรองข้อมูล Immich ที่เก็บเฉพาะคลังไฟล์แต่ไม่เก็บฐานข้อมูล จะทำให้กู้คืนได้เพียงไทม์ไลน์ที่ว่างเปล่า
การอัปเกรด: ทีละหนึ่งเวอร์ชันหลัก
Nextcloud รองรับการอัปเกรดทีละหนึ่งเวอร์ชันหลักเท่านั้น หากใช้ official Docker image การข้ามจากเวอร์ชัน 29 ไป 31 จะไม่เข้าสู่กระบวนการอัปเกรด เนื่องจาก entrypoint จะเปรียบเทียบเวอร์ชันหลักก่อนที่จะรัน occ upgrade จากนั้นจะแสดง Can't start Nextcloud because upgrading from <installed version> to <image version> is not supported. ตามด้วย It is only possible to upgrade one major version at a time. แล้วจึงปิดการทำงาน คอนเทนเนอร์จะไม่ยอมเริ่มทำงาน (หากใช้ restart: unless-stopped คอนเทนเนอร์จะรีสตาร์ทวนซ้ำและแสดงข้อความเดิม) โหมดบำรุงรักษาจะไม่ถูกเปิดใช้งาน และข้อมูลรวมถึงฐานข้อมูลจะยังคงไม่ถูกเปลี่ยนแปลง ให้ตั้งค่า tag เป็นเวอร์ชันหลักถัดไปแล้วเริ่มใหม่อีกครั้ง
ขั้นตอนการอัปเกรดผ่าน Docker คือ: สำรองข้อมูล แก้ไข tag ให้เป็นเวอร์ชันหลักถัดไปในบริการทั้ง app และ cron (จาก 35 เป็น 36 เมื่อเวอร์ชัน 36 ออก ซึ่ง Nextcloud วางแผนไว้ในเดือนกุมภาพันธ์ 2027) จากนั้นรัน docker compose pull && docker compose up -d แล้วตามด้วย docker compose logs -f app ตัว entrypoint ของ image จะตรวจพบโค้ดเวอร์ชันใหม่เทียบกับข้อมูลที่มีอยู่และรัน occ upgrade โดยอัตโนมัติ ห้ามขัดจังหวะกระบวนการนี้ เมื่อ log นิ่งแล้ว ให้รัน docker compose exec -u www-data app php occ status และตรวจสอบ versionstring รวมถึงตรวจสอบว่าแอปพลิเคชันต่างๆ กลับมาเปิดใช้งานตามปกติ
กฎสองข้อที่จะช่วยคุณได้คือ: ปรับเวอร์ชันหลักทีละขั้นแล้วตรวจสอบให้แน่ใจก่อนปรับขั้นถัดไป และห้ามแก้ไข tag ในบริการ app โดยไม่แก้ไข cron ให้ตรงกัน เพราะการใช้ Nextcloud สองเวอร์ชันที่แตกต่างกันกับฐานข้อมูลเดียวจะนำไปสู่ความเสียหายของข้อมูล
ข้อผิดพลาดที่คุณจะพบจริง
"Your data directory is readable by other people. Please change the permissions to 0770 so that the directory cannot be listed by other people." ตัวเลขหลักสุดท้ายของโหมดไดเรกทอรีซึ่งกำหนดสิทธิ์สำหรับผู้ใช้อื่นไม่ใช่ศูนย์ บิตการอ่าน เขียน หรือเรียกใช้งานสำหรับผู้อื่นจะทำให้เกิดข้อความนี้ บิตของกลุ่มจะไม่ถูกตรวจสอบเนื่องจาก 0770 ให้สิทธิ์เต็มที่แก่กลุ่มอยู่แล้ว Nextcloud จะพยายามรัน chmod 0770 ด้วยตัวเองก่อน และจะแจ้งเตือนเฉพาะเมื่อไม่สำเร็จ ซึ่งในกรณีของ bind mount มักหมายความว่าไดเรกทอรีไม่ได้เป็นของ UID 33 ดู sudo chown -R 33:33 /srv/nextcloud/data และ sudo chmod 0770 /srv/nextcloud/data
"Your data directory is invalid. Ensure there is a file called ".ncdata" in the root of the data directory." จุดเชื่อมต่อ bind mount ชี้ไปยังตำแหน่งที่ Nextcloud ไม่เคยเริ่มต้นใช้งาน, มีการพิมพ์ path ผิด หรือมีการสลับไดเรกทอรีว่างเปล่าใหม่เข้าไปแทนที่ instance ที่กำลังทำงานอยู่ ให้ตรวจสอบว่า path บนโฮสต์ตรงกับบรรทัด volume
"Access through untrusted domain." ชื่อโฮสต์ในคำขอไม่ได้อยู่ใน trusted_domains โดย NEXTCLOUD_TRUSTED_DOMAINS จะใช้เฉพาะตอนติดตั้งครั้งแรกเท่านั้น หลังจากนั้นให้ตั้งค่าแบบสดด้วยคำสั่ง: occ config:system:set trusted_domains 1 --value=cloud.example.com
502 Bad Gateway พร้อมกับ connect() failed (111: Connection refused) while connecting to upstream ใน /var/log/nginx/error.log แสดงว่า nginx ไม่สามารถเชื่อมต่อกับ 127.0.0.1:8080 ได้ อาจเป็นเพราะคอนเทนเนอร์กำลังเริ่มต้น (ตรวจสอบ docker compose logs app), คอนเทนเนอร์หยุดทำงานไปแล้ว (docker compose ps) หรือบรรทัด publish ไม่ตรงกับพอร์ต proxy_pass ให้ยืนยันด้วย ss -ltnp | grep 8080
เกิดการวนลูป redirect หรือคำเตือน "insecure" ในหน้าผู้ดูแลระบบ อาจเป็นเพราะ OVERWRITEPROTOCOL: https หายไป หรือ TRUSTED_PROXIES ไม่มี subnet ของ Docker gateway ให้ดูส่วนของ proxy ด้านบน
LockedException: "files/..." is locked หากตั้งค่า REDIS_HOST ไว้ อิมเมจจะกำหนดค่า Redis เป็น locking backend ทำให้เกิด stale locks ได้ยาก หากไม่มีการตั้งค่านี้ locks จะอยู่ในตารางฐานข้อมูล oc_file_locks และคำขอที่ถูกยกเลิกกลางคันระหว่างเขียนจะทิ้งแถวข้อมูลค้างไว้ ให้ยืนยันว่ามีการใช้งาน Redis จริง โดย occ config:system:get memcache.locking ควรส่งค่ากลับเป็นคลาส Redis ก่อนที่คุณจะลบแถว lock ด้วยตนเอง
"The PHP memory limit is below the recommended value of 512 MB." ให้เพิ่มค่า PHP_MEMORY_LIMIT แล้วสร้างคอนเทนเนอร์ใหม่ โปรดจำไว้ว่าการตั้งค่านี้ส่งผลต่อเพดานการใช้งานหน่วยความจำสูงสุดของคุณอย่างไร
สิ่งที่มักจะติดขัดเมื่อขยายขนาดระบบ
กำแพงด่านแรกคือขนาดของไดเรกทอรีข้อมูลที่ใหญ่เกินกว่าพื้นที่จัดเก็บข้อมูล (volume) ที่มีอยู่ การขยาย volume บน VPS ต้องทำทั้งการปรับขนาดพื้นที่และขยาย filesystem ซึ่งการวางแผนทำล่วงหน้าย่อมเจ็บปวดน้อยกว่าการรอให้พื้นที่เต็ม 100% ดังนั้นควรตั้งค่าการแจ้งเตือนการใช้งานดิสก์ไว้ตั้งแต่ตอนนี้ ไม่ใช่รอให้ถึงเวลาวิกฤต
กำแพงด่านที่สองคือ oc_filecache การแสดงรายการไฟล์และการสแกนเพื่อซิงค์ข้อมูลจะช้าลงเมื่อจำนวนแถวในฐานข้อมูลเพิ่มขึ้น วิธีแก้ไขคือการจัดการฐานข้อมูล: เก็บ Postgres ไว้บนที่จัดเก็บข้อมูลที่รวดเร็ว จัดสรรหน่วยความจำ shared memory ให้เพียงพอ และลบไฟล์ขยะหรือเวอร์ชันเก่าออกด้วยการตั้งค่า retention แทนที่จะปล่อยให้สะสมไปเรื่อยๆ
ด่านที่สามคือกระบวนการสร้างภาพตัวอย่าง (preview generation) ที่แย่งทรัพยากรกับงานส่วนอื่น บนเซิร์ฟเวอร์ขนาดเล็กควรจำกัดผู้ให้บริการ preview ให้เหลือน้อยที่สุด และห้ามรัน occ preview:generate-all ในช่วงเวลาทำงาน หากข้อมูลส่วนใหญ่ที่คุณจัดเก็บคือรูปภาพจากกล้องมือถือ งานสร้างภาพย่อ (thumbnail) เหล่านี้ควรไปทำบนเซิร์ฟเวอร์รูปภาพโดยเฉพาะแทน และ การเปรียบเทียบ PhotoPrism และ Immich ในด้าน RAM, แอปมือถือ และคำสั่งสำรองข้อมูล จะช่วยอธิบายต้นทุนของแต่ละตัวเมื่อเทียบกับการใช้ Nextcloud เพียงอย่างเดียว
นอกเหนือจากนั้น คำตอบที่ตรงไปตรงมาคือบริการเสริมเหล่านี้ต้องการเครื่องแยกต่างหาก Collabora และ full-text search เป็นบริการที่ทำงานค้างอยู่ในหน่วยความจำ (resident services) ซึ่งมีโปรไฟล์การใช้หน่วยความจำของตัวเอง การนำไปไว้บนเครื่องเดียวกับที่เก็บไฟล์ชุดเดียวของคุณจะยิ่งขยายขอบเขตความเสียหาย (failure domain) โดยไม่ได้รับประโยชน์ใดๆ หากคุณต้องการฟีเจอร์แก้ไขเอกสารในเบราว์เซอร์ ข้อกำหนดขั้นต่ำของ RAM และขีดจำกัดการเชื่อมต่อที่แยก OnlyOffice ออกจาก Collabora จะเป็นตัวตัดสินว่า VPS ขนาด 2 ถึง 4 GB จะรองรับตัวไหนได้บ้าง ให้ย้ายที่เก็บไฟล์ไปไว้บน S3-compatible primary storage เมื่อ volume เดิมไม่ตอบโจทย์ และโปรดทราบว่าวิธีนี้จะทำให้การสำรองข้อมูลยากขึ้น ไม่ใช่ง่ายขึ้น เพราะฐานข้อมูลยังคงเก็บ metadata อยู่ และต้องถูก dump ข้อมูลให้สอดคล้องกับ bucket เสมอ
เมื่อ instance เริ่มให้บริการผู้ใช้จริงแล้ว ให้ติดตั้ง Uptime Kuma ไว้หน้าบริการเพื่อให้คุณทราบถึงปัญหา downtime ก่อนที่ sync client จะแจ้งเตือน ระบบคลาวด์ส่วนตัวทำงานร่วมกับ เมลเซิร์ฟเวอร์ของคุณเอง ได้เป็นอย่างดี และหากคุณไม่ต้องการเชื่อมต่อบริการต่างๆ ด้วยตัวเอง Cloudron, CasaOS และ Coolify จะช่วยเปรียบเทียบแพลตฟอร์มที่จัดการเรื่องเหล่านี้แทนคุณ หากเป้าหมายถัดไปของคุณคือการทำ search engine แบบ self-hosted ให้เตรียมรับมือกับปัญหาอีกระดับที่ต่างจากข้างต้น: ข้อผิดพลาด 429 ของ SearXNG อาจเกิดจากตัวจำกัดอัตราการเข้าถึง (rate limiter) ของตัวมันเอง หรือเกิดจาก upstream engine บล็อก IP ของ VPS คุณ ซึ่งมีเพียง log เท่านั้นที่จะบอกได้ว่าสาเหตุมาจากอะไร
FAQ
ฉันสามารถรัน Nextcloud บน SQLite แทน Postgres ได้หรือไม่?
คุณสามารถทำได้และอิมเมจอย่างเป็นทางการก็รองรับ แต่ไคลเอนต์ซิงค์ข้อมูลบนเดสก์ท็อปเพียงเครื่องเดียวที่ส่งคำขอแบบขนานจะทำให้เกิด SQLSTATE[HY000]: General error: 5 database is locked และข้อผิดพลาด HTTP 500 เนื่องจาก SQLite จะล็อกการเขียนฐานข้อมูลทั้งชุดในขณะที่ Nextcloud มีการเขียนข้อมูลตลอดเวลา ทั้งการล็อกไฟล์, แถวข้อมูลกิจกรรม และสถานะงาน แนะนำให้เริ่มต้นด้วย Postgres หรือ MariaDB; แม้จะมี occ db:convert-type ให้ใช้งาน แต่ก็เป็นการย้ายข้อมูลที่ซับซ้อนและมีความเสี่ยงสูงหากทำบนข้อมูลจริง
Nextcloud VPS จำเป็นต้องใช้ RAM เท่าไหร่กันแน่?
ให้พิจารณาจากจำนวนการใช้งานพร้อมกัน (concurrency) ไม่ใช่จำนวนผู้ใช้ หน่วยความจำที่ใช้จริงในกรณีที่แย่ที่สุดจะเท่ากับจำนวนคำขอพร้อมกันคูณด้วย PHP_MEMORY_LIMIT บวกกับ shared buffers ของ Postgres และ backend หนึ่งตัวต่อการเชื่อมต่อ รวมถึงส่วนที่ใช้ในการสร้างตัวอย่างภาพ (preview generation) เซิร์ฟเวอร์ขนาด 2 GB สามารถรองรับการใช้งานในครัวเรือนขนาดเล็กได้หากคุณจำกัดการสร้าง preview และเพิ่ม swap หากคุณเพิ่ม Collabora หรือการค้นหาแบบ full-text เข้าไป คุณจะต้องจัดสรรทรัพยากรเพิ่มสำหรับบริการชุดที่สองเหล่านี้
ทำไมการอัปโหลดไฟล์ขนาดใหญ่ถึงล้มเหลวเมื่ออยู่หลัง nginx reverse proxy?
โดยปกติแล้วเกิดจากการตั้งค่าสองอย่างบน proxy หาก client_max_body_size ยังคงเป็นค่าเริ่มต้นที่ 1 MB ตัว nginx จะปฏิเสธคำขอที่ใหญ่กว่านั้นทันทีด้วยข้อผิดพลาด 413 (Request Entity Too Large) ซึ่งจะปรากฏใน log ของ nginx แทนที่จะเป็นไฟล์ที่อัปโหลดไม่สมบูรณ์ นอกจากนี้ค่า proxy_read_timeout / proxy_send_timeout ที่สั้นเกินไปจะทำให้การโอนถ่ายข้อมูลที่ใช้เวลานานถูกตัดการเชื่อมต่อกลางคัน ให้ตั้งค่าทั้งสองอย่างให้สูงขึ้น ปรับ proxy_request_buffering off ให้เป็นแบบ stream แทนการ spool และเพิ่มค่า PHP_UPLOAD_LIMIT บน container ของแอปให้สอดคล้องกัน
ทำไม Nextcloud ถึงเกิดการ redirect วนลูปหรือแจ้งเตือนเกี่ยวกับ reverse proxy?
ตัว container มองไม่เห็น nginx ที่ 127.0.0.1 แต่จะเห็นเป็น Docker bridge gateway ซึ่งอยู่ในช่วง 172.x เมื่อที่อยู่ดังกล่าวไม่ได้ถูกระบุไว้ใน TRUSTED_PROXIES ตัว header X-Forwarded-Proto: https จะถูกละเลย ทำให้ Nextcloud สร้าง URL เป็น http:// และตัว proxy จะส่งคำขอกลับไปกลับมา ให้ตั้งค่า TRUSTED_PROXIES เป็น subnet ของ bridge จริงและกำหนด OVERWRITEPROTOCOL: https ให้คงที่
ฉันสามารถอัปเกรด Nextcloud จากเวอร์ชัน 29 ไปเป็น 31 โดยตรงได้หรือไม่?
ไม่ได้ Nextcloud รองรับการอัปเกรดข้ามเวอร์ชันหลักได้ทีละหนึ่งเวอร์ชันเท่านั้น ในอิมเมจ Docker อย่างเป็นทางการ ตัว entrypoint จะตรวจสอบเวอร์ชันหลักก่อนที่จะรัน occ upgrade หากไม่ผ่านจะแสดง It is only possible to upgrade one major version at a time. และหยุดทำงาน ทำให้ container ไม่เริ่มทำงานและข้อมูลรวมถึงฐานข้อมูลของคุณจะยังคงอยู่ในสถานะเดิม ให้เปลี่ยน tag เป็น 30 แทน: ทำการสำรองข้อมูล, ปรับ tag ขึ้นหนึ่งเวอร์ชันหลักทั้งในบริการ app และ cron, ทำการ docker compose pull && docker compose up -d, ตรวจสอบความถูกต้องด้วย occ status แล้วจึงทำซ้ำขั้นตอนเดิมอีกครั้ง