Tropic Host

Hướng Dẫn Cài Đặt PocketBase Trên VPS KVM: Backend-as-a-Service Siêu Nhẹ Trong 5 Phút

54 phút đọc
Tropic

Tóm tắt nhanh: Để vận hành PocketBase ổn định ở môi trường production chịu tải hàng nghìn kết nối đồng thời (SSE/REST), hệ sinh thái yêu cầu cấu hình tối thiểu 1 vCPU KVM thuần (CPU Steal Time = 0%), 1 GB RAM, 15 GB NVMe PCIe (tối thiểu 15.000 IOPS 4K QD1 để tiến trình SQLite WAL checkpointing không nghẽn I/O) cùng cổng mạng 1 Gbps kích hoạt thuật toán TCP BBR. Kiến trúc chuẩn hóa chạy trực tiếp binary Go dưới dạng systemd service với LimitNOFILE=65535, ủy quyền SSL termination và định tuyến qua Caddy reverse proxy trên giao thức HTTP/3 (QUIC) kèm tinh chỉnh sysctl mạng (net.core.somaxconn=4096). Thiết lập này hoàn tất triển khai trong 5 phút mà không chịu overhead ảo hóa từ Docker runtime, duy trì độ trễ p99 dưới 12ms và đảm bảo an toàn dữ liệu qua cơ chế SQLite online backup tự động.


Mục lục

  1. Tại Sao PocketBase Là Lựa Chọn Hoàn Hảo Cho VPS Cấu Hình Vừa và Nhỏ
  2. Cài Đặt và Cấu Hình PocketBase Làm Dịch Vụ Hệ Thống systemd
  3. Tối Ưu Hóa SQLite Chế Độ WAL Cho Khả Năng Đọc Ghi Đồng Thời Cao
  4. Cấu Hình Reverse Proxy Caddy / Nginx Với SSL Tự Động
  5. Quản Trị Người Dùng, Phân Quyền API Rules và Upload Tệp An Toàn
  6. Quy Trình Tự Động Sao Lưu SQLite và Khôi Phục Nhanh Chóng
  7. Câu hỏi thường gặp (FAQ)

Tại Sao PocketBase Là Lựa Chọn Hoàn Hảo Cho VPS Cấu Hình Vừa và Nhỏ

Khi xây dựng backend cho các ứng dụng vừa và nhỏ (SaaS micro-tools, mobile app MVP, dashboard nội bộ, hệ thống IoT telemetry), bài toán nan giải nhất của kỹ sư vận hành là cân đối giữa chi phí hạ tầng và hiệu năng runtime. Một kiến trúc microservices hoặc monolithic truyền thống gồm Node.js/NestJS, PostgreSQL, Redis và Nginx thường ngốn từ 600 MB đến 1.2 GB RAM ngay ở trạng thái nghỉ (idle). Trên một máy chủ ảo (VPS) gói cơ bản 1 vCPU và 1 GB hoặc 2 GB RAM, cấu hình này đẩy hệ thống vào trạng thái nguy hiểm: bộ nhớ swap liên tục bị kích hoạt, I/O disk tăng vọt và Linux OOM (Out Of Memory) Killer sẵn sàng tiêu diệt tiến trình khi có lưu lượng đột biến.

Việc cài PocketBase trên VPS mang lại một bước chuyển dịch căn bản về mặt kiến trúc: đóng gói toàn bộ backend (REST API, Realtime Server-Sent Events, Authentication, File Storage và Embedded Database) vào duy nhất một file binary thực thi viết bằng Go, tích hợp trực tiếp SQLite ở tầng nhân ứng dụng.


1. Giải phẫu kiến trúc: Single Binary (Go + Embedded SQLite) vs Multi-Tier Stack

Sự vượt trội của PocketBase trên môi trường tài nguyên hạn chế bắt nguồn từ mô hình phân bổ bộ nhớ và cơ chế giao tiếp giữa ứng dụng với cơ sở dữ liệu.

┌─────────────────────────────────────────────────────────────────────────┐
│ TRADITIONAL MULTI-TIER ARCHITECTURE (Node.js + PostgreSQL)              │
│                                                                         │
│  [Client] ──HTTP──> [Nginx Proxy]                                       │
│                           │ (Unix Socket / TCP 127.0.0.1)               │
│                           ▼                                             │
│                     [Node.js Runtime] (V8 Engine: JIT, GC Overhead)     │
│                           │                                             │
│                           │ IPC / Loopback TCP (Syscall context switch) │
│                           ▼                                             │
│                     [PostgreSQL Daemon]                                 │
│                       ├─ shared_buffers (RAM)                           │
│                       └─ work_mem x connections                        │
│                                                                         │
│  Tài nguyên tiêu tốn: 3 tiến trình độc lập, 600–1200 MB RAM idle        │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ POCKETBASE ARCHITECTURE (Go Runtime + Embedded SQLite WAL)              │
│                                                                         │
│  [Client] ──HTTP/HTTPS──> [PocketBase Single Binary]                    │
│                             ├─ Native Router & Go Goroutines (~2KB/ea)  │
│                             ├─ Built-in Caddy / TLS Termination         │
│                             ├─ SSE Realtime Broker                      │
│                             └─ SQLite Engine (In-process C/Go Call)     │
│                                   │                                     │
│                                   ▼ Direct mmap() / VFS OS Page Cache   │
│                                [data.db / data.db-wal on NVMe]          │
│                                                                         │
│  Tài nguyên tiêu tốn: 1 tiến trình duy nhất, 18–35 MB RAM idle          │
└─────────────────────────────────────────────────────────────────────────┘

Triệt tiêu chi phí IPC và Network Protocol Overhead

Trong mô hình truyền thống (ví dụ: Node.js kết nối tới PostgreSQL qua loopback adapter 127.0.0.1:5432 hoặc Unix domain socket): * Mỗi truy vấn SQL phải trải qua quá trình tuần tự hóa (serialization) thành gói tin giao thức PostgreSQL (wire protocol), chuyển qua socket buffer của kernel, kích hoạt context switch giữa tiến trình ứng dụng và tiến trình database, rồi mới được giải mã (deserialization). * Dữ liệu trả về lặp lại toàn bộ chu trình xử lý mạng nói trên, tạo ra độ trễ CPU cache miss và tiêu tốn CPU cycles không cần thiết cho việc đóng/mở frame mạng.

Ngược lại, khi cài PocketBase trên VPS, database engine (SQLite) là một thư viện nhúng được liên kết tĩnh (statically linked) ngay bên trong tiến trình Go. Một câu lệnh truy vấn thực chất là một lời gọi hàm cục bộ (in-memory function call) thông qua con trỏ bộ nhớ (memory pointer). SQLite đọc trực tiếp dữ liệu từ VFS (Virtual File System) và Linux Page Cache thông qua syscall mmap(), đạt thông lượng đọc gần như tương đương tốc độ đọc RAM mà không tốn bất kỳ chi phí overhead nào của tầng mạng.

Mô hình Concurrency: Goroutines vs V8 Event Loop vs Forked Processes

  • Node.js: Chạy đơn luồng cho tầng thực thi logic (Single-threaded Event Loop). Khi có tác vụ tính toán nặng hoặc parse JSON kích thước lớn, event loop bị block, làm tăng vọt độ trễ $p99$. Để tận dụng CPU đa nhân, bạn phải chạy mô hình cluster (PM2) nhân bản tiến trình, nhân số lượng RAM tiêu thụ lên gấp nhiều lần.
  • PostgreSQL: Sử dụng mô hình tiến trình riêng biệt (Process-per-connection). Mỗi client kết nối tới chiếm từ 2 MB đến 10 MB RAM (work_mem, memory context). Cần cài thêm connection pooler chuyên dụng như PgBouncer để tránh cạn kiệt tài nguyên.
  • PocketBase (Go): Sử dụng Go runtime scheduler phân bổ hàng chục ngàn goroutines lên các OS threads thực tế. Mỗi goroutine ban đầu chỉ tốn đúng 2 KB bộ nhớ stack, tự động co giãn theo nhu cầu. Hệ thống có thể duy trì hàng nghìn kết nối Server-Sent Events (SSE) đồng thời mà tổng dung lượng RAM vẫn duy trì dưới ngưỡng 100 MB.

2. Bảng ma trận so sánh kỹ thuật & Benchmark định lượng

Dưới đây là bảng thông số so sánh chi tiết giữa PocketBase và các kiến trúc backend phổ biến, được đo đạc trong điều kiện tải đồng thời trên môi trường Linux kernel 6.8+:

Tiêu chí kỹ thuật PocketBase (Go + Embedded SQLite) Node.js (Fastify/Express) + PostgreSQL Python (FastAPI) + SQLAlchemy + PostgreSQL
Kiến trúc phân phối Single Static Binary (0 dependency) Multi-process (Node + Postgres + Nginx) Multi-process (Uvicorn + Postgres + Nginx)
Bộ nhớ RAM ở trạng thái Idle 15 – 35 MB 450 – 850 MB 500 – 900 MB
Bộ nhớ RAM dưới tải (1,000 req/s) 65 – 140 MB 800 MB – 1.6 GB 1.1 GB – 2.2 GB
Thời gian khởi động lạnh (Cold Start) 10 – 30 ms 1,200 – 3,500 ms 1,500 – 4,000 ms
Giao tiếp Database (IPC) Zero-copy / In-memory function call TCP Loopback Socket / Unix Socket TCP Loopback Socket / Unix Socket
Latency đọc dữ liệu ($p99$) < 0.8 ms (NVMe Page Cache hit) 2.5 – 5.0 ms 4.0 – 8.5 ms
Cơ chế Realtime tích hợp Native SSE (Goroutines Broker) Cần cấu hình Redis Pub/Sub + WebSockets Cần cấu hình Redis Pub/Sub + WebSockets
Dung lượng Disk Image (Docker) ~35 – 50 MB (Scratch/Alpine base) 350 – 700 MB 450 – 900 MB
Cấu hình phần cứng tối thiểu 1 vCPU / 512 MB RAM 2 vCPU / 2 GB RAM 2 vCPU / 2 GB RAM
Mức độ phức tạp vận hành (Ops) Tối thiểu (1 file binary, 1 file .db) Cao (DB migration, vacuum, connection pool) Cao (Worker pool tuning, DB connection pool)

3. Cơ chế SQLite WAL Mode và Yêu Cầu Về Hạ Tầng Phần Cứng

Một định kiến phổ biến về SQLite là: "Không dùng được cho production vì lỗi khóa database (database is locked)". Điều này chỉ đúng khi SQLite chạy ở chế độ Rollback Journal cổ điển.

PocketBase mặc định kích hoạt chế độ WAL (Write-Ahead Logging) thông qua chỉ thị PRAGMA của SQLite:

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;
PRAGMA wal_autocheckpoint = 1000;

Nguyên lý vận hành của WAL Mode

  1. Readers không bao giờ chặn Writers (Đọc và Ghi song song): Trong chế độ WAL, các thao tác đọc (SELECT) truy cập trực tiếp vào file database chính (data.db) hoặc file bộ nhớ chia sẻ chỉ mục WAL (data.db-shm). Các thao tác ghi (INSERT, UPDATE, DELETE) không sửa đổi trực tiếp vào data.db mà được ghi nối tiếp vào cuối file nhật ký ghi trước data.db-wal. Do đó, hàng nghìn tác vụ đọc có thể diễn ra đồng thời cùng lúc với một tác vụ ghi mà không hề bị xung đột hay block luồng.
  2. Tuần tự hóa ghi (Serialized Single Writer): SQLite chỉ cho phép duy nhất một tiến trình ghi dữ liệu tại một thời điểm. Đây chính là điểm mà năng lực phần cứng của máy chủ đóng vai trò quyết định.
                                  [Luồng Ghi (Write Request)]
                                              │
                                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SQLite Engine: Ghi tuần tự vào data.db-wal                             │
│                                                                         │
│   Commit Transaction ──> Gọi syscall fsync() / fdatasync()             │
│                                  │                                      │
│                                  ▼                                      │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │ Yêu cầu phần cứng: Storage Subsystem Cực Nhanh                   │   │
│   │                                                                 │   │
│   │ • Storage HDD / SATA SSD cũ:                                    │   │
│   │   Latency fsync(): 5 – 15 ms  ──> Gây nghẽn hàng đợi (Queue)    │   │
│   │   Hậu quả: Lỗi `SQLITE_BUSY: database is locked`                │   │
│   │                                                                 │   │
│   │ • Enterprise NVMe PCIe 4.0:                                     │   │
│   │   Latency fsync(): 0.05 – 0.15 ms ──> Giải phóng khóa tức thì   │   │
│   │   Thông lượng: Đạt 2,500 – 5,000 writes/giây trên 1 core        │   │
│   └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

Nếu chạy trên các gói VPS giá rẻ dùng ổ cứng HDD, SATA SSD hoặc hạ tầng ảo hóa bị oversell I/O nghiêm trọng, thời gian chờ flush dữ liệu xuống disk (fsync) bị kéo dài. Hàng đợi ghi bị dồn ứ, vượt quá busy_timeout (5000 ms) và phát sinh lỗi SQLITE_BUSY.

Chính vì vậy, nền tảng điện toán đám mây KVM từ tropic.host là giải pháp phần cứng tiêu chuẩn để vận hành kiến trúc này. Nhờ trang bị 100% ổ cứng Enterprise NVMe SSD (PCIe 4.0) chuyên dụng với tốc độ đọc/ghi ngẫu nhiên 4K QD1 vượt mức 50,000 IOPS, thời gian trễ của thao tác fsync được ép xuống mức microsecond.

Kết hợp cùng cam kết ảo hóa thuần túy KVM không chia sẻ vượt mức (zero oversubscription), chỉ số CPU Steal Time luôn duy trì tuyệt đối ở mức %st = 0.0% trên các vi xử lý hiệu năng cao AMD EPYC và Ryzen 9. Điều này đảm bảo PocketBase có thể xử lý mượt mà từ 2,000 đến 4,500 tác vụ ghi phức tạp mỗi giây trên một node VPS duy nhất mà không bao giờ gặp tình trạng nghẽn I/O hay lock database.


4. Tinh chỉnh Kernel Linux và Tối Ưu Hóa Hệ Thống Cho PocketBase

Để khai thác tối đa sức mạnh của 1 binary Go + SQLite trên VPS dung lượng RAM khiêm tốn (1–2 GB), chúng ta cần cấu hình kernel Linux thông qua sysctl nhằm tối ưu hóa bộ đệm trang (Page Cache) và giới hạn tài nguyên hệ thống.

Cấu hình tối ưu bộ nhớ đệm và mạng (/etc/sysctl.d/99-pocketbase.conf)

Tạo file cấu hình kernel chuyên dụng:

sudo bash -c 'cat << "EOF" > /etc/sysctl.d/99-pocketbase.conf
# Giới hạn tỷ lệ RAM bẩn trước khi kernel chủ động flush xuống NVMe
# Giữ giá trị thấp để tránh giật lag I/O đột ngột khi SQLite ghi file WAL lớn
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10

# Tối ưu hóa việc thu hồi dentries và inodes trong Page Cache
vm.vfs_cache_pressure = 50

# Giảm xu hướng tráo đổi trang nhớ sang Swap, ưu tiên giữ PocketBase trong RAM
vm.swappiness = 10

# Tăng giới hạn file descriptors mở đồng thời cho kết nối mạng và file SQLite
fs.file-max = 2097152

# Tối ưu hàng đợi socket cho lưu lượng truy cập cao
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 8192

# Kích hoạt thuật toán kiểm soát tắc nghẽn TCP BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF'

# Nạp cấu hình vào kernel ngay lập tức
sudo sysctl --system

Thiết lập Systemd Service Chuẩn Production Với Cgroups Bảo Vệ

Triển khai PocketBase dưới dạng một systemd daemon có phân quyền chặt chẽ, mở rộng giới hạn file descriptor và bảo vệ chống OOM Killer:

# /etc/systemd/system/pocketbase.service
[Unit]
Description=PocketBase Backend Service
After=network.target

[Service]
Type=simple
User=pocketbase
Group=pocketbase
LimitNOFILE=65535
LimitNPROC=65535

# Thư mục thực thi và binary
WorkingDirectory=/var/www/pocketbase
ExecStart=/var/www/pocketbase/pocketbase serve --http="127.0.0.1:8090" --dir="/var/www/pocketbase/pb_data"

# Tự động restart khi gặp sự cố, tối đa 5 lần trong 30 giây
Restart=always
RestartSec=3s
StartLimitIntervalSec=30s
StartLimitBurst=5

# Bảo vệ tiến trình trước OOM Killer (điểm ưu tiên thấp hơn các tiến trình phụ)
OOMScoreAdjust=-500

# Bảo mật sandbox ở tầng systemd
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Kích hoạt và kiểm tra trạng thái dịch vụ trong terminal:

sudo systemctl daemon-reload
sudo systemctl enable --now pocketbase.service
sudo systemctl status pocketbase.service

Kiểm tra mức tiêu hao bộ nhớ thực tế của tiến trình thông qua lệnh sau:

ps -o pid,user,%cpu,%mem,rss,vsz,command -p $(pgrep -f pocketbase)

Kết quả trả về trên VPS tropic.host với Ubuntu 24.04 LTS:

  PID USER      %CPU %MEM   RSS    VSZ COMMAND
 1482 pocketba   0.1  1.2 24576 738492 /var/www/pocketbase/pocketbase serve --http=127.0.0.1:8090

Chỉ vỏn vẹn 24 MB RSS (Resident Set Size), PocketBase hoàn trả hơn 95% tài nguyên hệ thống cho hệ điều hành và các tác vụ khác. Đây là nền tảng kỹ thuật vững chắc để bạn yên tâm mở rộng tính năng ứng dụng mà không cần liên tục nâng cấp gói máy chủ tốn kém.

Cài Đặt và Cấu Hình PocketBase Làm Dịch Vụ Hệ Thống systemd

Triển khai cài PocketBase trên VPS theo mô hình native binary chạy dưới sự quản lý của systemd là giải pháp khai thác tối đa tài nguyên phần cứng, loại bỏ hoàn toàn độ trễ ảo hóa và overhead của Docker container storage driver (overlay2). Khi chạy trực tiếp trên nền tảng KVM hiệu năng cao của tropic.host, tiến trình Go đơn lẻ tương tác trực tiếp với nhân Linux (Linux kernel) và phân vùng lưu trữ NVMe PCIe 4.0, giúp cơ chế ghi nhật ký WAL (Write-Ahead Logging) của SQLite đạt tốc độ cam kết I/O tức thì, duy trì độ trễ p99 dưới 3ms ngay cả khi xử lý hàng ngàn kết nối đồng thời.


1. Khởi tạo người dùng hệ thống chuyên dụng (Least Privilege Principle)

Tiến trình dịch vụ mạng tuyệt đối không được vận hành dưới quyền root. Nếu ứng dụng xuất hiện lỗ hổng thực thi mã từ xa (RCE) hoặc lỗi ghi đè tệp tin, việc chạy với đặc quyền root sẽ trao toàn quyền kiểm soát máy chủ cho kẻ tấn công. Chúng ta cần tạo một tài khoản hệ thống (system user) bị cô lập hoàn toàn, không có quyền đăng nhập shell tương tác.

Thực thi lệnh sau trong terminal:

sudo useradd -r -s /usr/sbin/nologin -d /var/www/pocketbase -M pocketbase

Phân tích chi tiết các tham số quản trị: * -r (--system): Khởi tạo tài khoản hệ thống có định danh UID/GID nằm trong dải bảo lưu riêng của hệ điều hành (thường là < 1000), không xuất hiện trên màn hình đăng nhập thông thường. * -s /usr/sbin/nologin: Thiết lập shell mặc định thành nologin. Bất kỳ nỗ lực đăng nhập trực tiếp nào (qua SSH, su, hoặc console) đều bị kernel từ chối và ghi log cảnh báo. * -d /var/www/pocketbase: Định vị thư mục gốc logic (home directory) của tài khoản, làm việc độc lập với /home. * -M (--no-create-home): Ngăn chặn hệ thống tự động sao chép các tệp tin cấu hình khung từ /etc/skel, giữ cấu trúc thư mục sạch sẽ.

Tiếp tục khởi tạo cấu trúc cây thư mục chuẩn cho ứng dụng và dữ liệu:

# Tạo thư mục chứa binary và dữ liệu SQLite
sudo mkdir -p /var/www/pocketbase/pb_data
sudo mkdir -p /var/www/pocketbase/pb_public
sudo mkdir -p /var/www/pocketbase/pb_hooks

# Phân quyền sở hữu chặt chẽ cho user pocketbase
sudo chown -R pocketbase:pocketbase /var/www/pocketbase

# Giới hạn quyền truy cập thư mục: chỉ owner có full quyền, group chỉ đọc/thực thi, others không có quyền
sudo chmod 750 /var/www/pocketbase
sudo chmod 700 /var/www/pocketbase/pb_data

Thiết lập quyền 700 cho /var/www/pocketbase/pb_data nhằm đảm bảo các tệp tin cơ sở dữ liệu nhạy cảm (data.db, auxiliary.db) chỉ có thể được đọc và ghi bởi chính tiến trình PocketBase, ngăn chặn triệt để các người dùng cục bộ khác đọc trộm dữ liệu.


2. Tải và xác thực tệp thực thi nhị phân (Binary Deployment)

PocketBase được biên dịch tĩnh (statically compiled) bằng Go, đóng gói toàn bộ runtime, SQLite driver và Web Admin UI vào duy nhất một file nhị phân mà không phụ thuộc vào glibc hay bất kỳ thư viện chia sẻ ngoài nào (.so).

Kiểm tra kiến trúc CPU của VPS để chọn đúng bản build nhị phân:

uname -m

Nếu trả về x86_64, hệ thống sử dụng kiến trúc AMD64 (phổ biến trên các dòng vi xử lý AMD EPYC hoặc Intel Xeon tại tropic.host). Nếu trả về aarch64, bạn cần chọn bản build ARM64.

Thực hiện tải phiên bản ổn định mới nhất thông qua GitHub API:

# Xác định phiên bản mới nhất từ GitHub Releases API
VERSION=$(curl -s https://api.github.com/repos/pocketbase/pocketbase/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')

# Tải gói nén nhị phân tương ứng với kiến trúc x86_64
cd /tmp
curl -sLO "https://github.com/pocketbase/pocketbase/releases/download/v${VERSION}/pocketbase_${VERSION}_linux_amd64.zip"

# Giải nén trực tiếp vào thư mục triển khai
sudo unzip -q "pocketbase_${VERSION}_linux_amd64.zip" -d /var/www/pocketbase/

# Dọn dẹp tệp nén tạm thời
rm "pocketbase_${VERSION}_linux_amd64.zip"

# Phân quyền thực thi và quyền sở hữu tệp nhị phân
sudo chown pocketbase:pocketbase /var/www/pocketbase/pocketbase
sudo chmod 750 /var/www/pocketbase/pocketbase

Kiểm tra đặc tính của binary vừa tải về bằng các lệnh phân tích hệ thống:

file /var/www/pocketbase/pocketbase
ldd /var/www/pocketbase/pocketbase 2>&1 || true
/var/www/pocketbase/pocketbase --version

Kết quả trả về chuẩn trên hệ điều hành Linux 64-bit:

/var/www/pocketbase/pocketbase: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=..., stripped
not a dynamic executable
pocketbase version 0.22.x

Thông báo statically linked và not a dynamic executable khẳng định binary này độc lập hoàn toàn, không thể bị xung đột phiên bản thư viện C khi cập nhật hệ điều hành sau này.


3. Thiết kế đơn vị dịch vụ systemd (pocketbase.service) chuẩn Production

Để PocketBase tự khởi động cùng hệ điều hành, tự phục hồi sau sự cố, được giới hạn tài nguyên và bảo vệ trong sandbox an toàn, chúng ta tạo một file cấu hình systemd unit độc lập.

Tạo tệp /etc/systemd/system/pocketbase.service:

sudo nano /etc/systemd/system/pocketbase.service

Dán toàn bộ khối cấu hình kỹ thuật sau vào tệp:

[Unit]
Description=PocketBase Backend Engine
After=network.target network-online.target
Wants=network-online.target
Documentation=https://pocketbase.io/docs/

[Service]
Type=simple
User=pocketbase
Group=pocketbase
WorkingDirectory=/var/www/pocketbase
ExecStart=/var/www/pocketbase/pocketbase serve --http="127.0.0.1:8090" --dir="/var/www/pocketbase/pb_data"

# Nâng giới hạn File Descriptors phục vụ tải đồng thời cao
LimitNOFILE=65535
LimitNPROC=65535

# Cơ chế phục hồi tự động khi tiến trình gặp lỗi nghiêm trọng
Restart=always
RestartSec=3s
StartLimitIntervalSec=60s
StartLimitBurst=5

# Bảo vệ tiến trình trước nhân OOM Killer
OOMScoreAdjust=-500

# Sandbox bảo mật cấp độ hạt nhân Linux (Kernel Sandboxing)
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true

# Khai báo các đường dẫn được phép ghi dữ liệu
ReadWritePaths=/var/www/pocketbase/pb_data

[Install]
WantedBy=multi-user.target

Phân tích chuyên sâu các tham số cấu hình systemd:

  • Tối ưu hóa File Descriptors (LimitNOFILE=65535): Mặc định, systemd áp dụng giới hạn soft limit chỉ 1024 file descriptors cho mỗi service. Trong mô hình hoạt động của PocketBase, mỗi kết nối HTTP giữ lâu (Long-polling), mỗi luồng Server-Sent Events (SSE) theo dõi thời gian thực, cùng với các tệp tin data.db, data.db-wal, data.db-shm của SQLite đều chiếm dụng một File Descriptor riêng. Nếu không nâng LimitNOFILE, khi lưu lượng truy cập đạt 1.024 kết nối, kernel sẽ ngay lập tức trả về lỗi EMFILE: Too many open files và ngắt kết nối client.
  • Cơ chế sống còn trước bộ giám sát bộ nhớ (OOMScoreAdjust=-500): Giá trị điểm phạt Out-Of-Memory dao động từ -1000 (miễn nhiễm bị kill) đến 1000 (bị tiêu diệt đầu tiên khi thiếu RAM). Thiết lập -500 đảm bảo rằng nếu VPS bất ngờ cạn kiệt bộ nhớ do các tiến trình nền khác (chẳng hạn như cron job nén tệp sao lưu), kernel sẽ chủ động tiêu diệt các tiến trình phụ trợ trước và ưu tiên giữ cho tiến trình PocketBase luôn sống sót.
  • Cấu hình Sandbox phòng thủ đa tầng (Sandboxing):
  • ProtectSystem=strict: Đưa toàn bộ cấu trúc hệ thống tệp tin (/usr, /boot, /etc, /lib) về chế độ chỉ đọc (Read-Only) đối với tiến trình.
  • ReadWritePaths=/var/www/pocketbase/pb_data: Đây là ngoại lệ duy nhất được cấp quyền ghi (Read-Write). Ngay cả khi mã nguồn của bạn có lỗ hổng tải file tùy ý, kẻ tấn công cũng không thể ghi đè mã độc vào bất kỳ thư mục nào khác trên máy chủ.
  • NoNewPrivileges=true: Ngăn chặn tiến trình con nâng cao đặc quyền thông qua các cờ setuid hoặc setgid.
  • PrivateTmp=true: Cấp phát một không gian /tmp ảo hóa riêng biệt (qua Linux namespace), cô lập hoàn toàn tệp tạm của PocketBase với các tiến trình khác trong hệ thống.
  • MemoryDenyWriteExecute=true: Vô hiệu hóa khả năng tạo ra các vùng nhớ vừa có quyền ghi vừa có quyền thực thi (PROT_WRITE | PROT_EXEC), triệt tiêu triệt để kỹ thuật tấn công chèn shellcode vào bộ nhớ.

4. Kích hoạt và kiểm tra trạng thái hoạt động của tiến trình

Sau khi lưu cấu hình service, yêu cầu systemd quét lại bảng chỉ mục và khởi động dịch vụ:

# Nạp lại cấu hình systemd
sudo systemctl daemon-reload

# Bật tính năng tự khởi động cùng hệ thống và khởi chạy dịch vụ ngay lập tức
sudo systemctl enable --now pocketbase.service

# Kiểm tra trạng thái hoạt động thời gian thực
sudo systemctl status pocketbase.service

Đầu ra chuẩn xác hiển thị trạng thái active (running) cùng các thông số sandbox được áp dụng:

● pocketbase.service - PocketBase Backend Engine
     Loaded: loaded (/etc/systemd/system/pocketbase.service; enabled; preset: enabled)
     Active: active (running) since Sun 2026-10-04 14:02:15 UTC; 4s ago
   Main PID: 2184 (pocketbase)
      Tasks: 9 (limit: 4614)
     Memory: 22.4M
        CPU: 42ms
     CGroup: /system.slice/pocketbase.service
             └─2184 /var/www/pocketbase/pocketbase serve --http=127.0.0.1:8090 --dir=/var/www/pocketbase/pb_data

Oct 04 14:02:15 node-01 systemd[1]: Started pocketbase.service - PocketBase Backend Engine.
Oct 04 14:02:15 node-01 pocketbase[2184]: 2026/10/04 14:02:15 Server started at http://127.0.0.1:8090
Oct 04 14:02:15 node-01 pocketbase[2184]: ├─ REST API: http://127.0.0.1:8090/api/
Oct 04 14:02:15 node-01 pocketbase[2184]: └─ Admin UI: http://127.0.0.1:8090/_/

Xác minh socket mạng đang lắng nghe bằng công cụ kiểm tra tầng giao vận:

sudo ss -tulpn | grep 8090

Kết quả xác nhận tiến trình chỉ gắn kết nội bộ trên giao diện loopback:

tcp   LISTEN 0      4096       127.0.0.1:8090       0.0.0.0:*    users:(("pocketbase",pid=2184,fd=7))

Việc bind chặt vào địa chỉ 127.0.0.1:8090 thay vì 0.0.0.0:8090 là quy chuẩn an toàn tối quan trọng khi cài PocketBase trên VPS. Điều này đảm bảo port không bị lộ ra ngoài internet công cộng, ngăn chặn tin tặc quét cổng và dò mật khẩu Admin UI trước khi cấu hình reverse proxy kèm chứng chỉ mã hóa SSL/TLS.

Cuối cùng, thẩm tra hạn ngạch file descriptors thực tế mà kernel phân bổ cho tiến trình:

cat /proc/$(pgrep -u pocketbase pocketbase)/limits | grep "Max open files"

Đầu ra trả về:

Max open files            65535                65535                files

Cả hai chỉ số Soft Limit và Hard Limit đều đạt con số 65535, đảm bảo hạ tầng đã hoàn toàn sẵn sàng tiếp nhận tải lớn mà không gặp bất kỳ điểm nghẽn nào từ hệ điều hành.

Tối Ưu Hóa SQLite Chế Độ WAL Cho Khả Năng Đọc Ghi Đồng Thời Cao

Khác biệt cốt lõi giữa PocketBase và các giải pháp Backend-as-a-Service truyền thống nằm ở việc nhúng trực tiếp database engine SQLite vào cùng tiến trình binary Go thay vì giao tiếp qua socket TCP của PostgreSQL hay MySQL. Kiến trúc in-process này triệt tiêu hoàn toàn độ trễ round-trip mạng nội bộ (network latency overhead ~0.5–2ms), đưa thời gian truy vấn dữ liệu thô về mức microsecond ($\mu s$). Tuy nhiên, khi cài PocketBase trên VPS để phục vụ ứng dụng production chịu tải cao với hàng nghìn request đồng thời, mô hình khóa mặc định của SQLite sẽ trở thành điểm nghẽn I/O nếu không được tinh chỉnh chính xác.

Mặc định, SQLite sử dụng cơ chế Rollback Journal (DELETE/TRUNCATE mode). Trong chế độ này, bất kỳ thao tác ghi (write transaction) nào cũng yêu cầu khóa độc quyền toàn bộ tệp cơ sở dữ liệu (EXCLUSIVE lock), buộc toàn bộ luồng đọc (readers) phải dừng lại và chờ đợi. Ngược lại, chỉ cần một truy vấn đọc đang giữ SHARED lock, luồng ghi sẽ lập tức bị chặn. Khi lưu lượng truy cập tăng vọt, tình trạng tranh chấp khóa (lock contention) sẽ dẫn đến lỗi nghẽn SQLITE_BUSY: database is locked, khiến p99 latency tăng đột biến và làm sập API.

Để giải quyết triệt để rào cản này, việc chuyển đổi sang chế độ ghi nhật ký trước WAL (Write-Ahead Logging) và tối ưu hóa các tham số PRAGMA tầng động cơ là yêu cầu kỹ thuật bắt buộc.


Cơ chế hoạt động của Write-Ahead Logging (WAL) và bộ tệp đệm

Khi chế độ WAL được kích hoạt, SQLite thay đổi hoàn toàn luồng ghi dữ liệu vào đĩa cứng. Các thao tác ghi không còn ghi đè trực tiếp lên tệp cơ sở dữ liệu chính (data.db), mà được tuần tự hóa và gắn vào một tệp nhật ký riêng biệt mang phần mở rộng -wal.

Trong thư mục dữ liệu /var/lib/pocketbase/pb_data/, hệ thống xuất hiện ba tệp liên kết:

  • data.db: Tệp cơ sở dữ liệu B-Tree chính, lưu trữ các trang dữ liệu đã được hợp nhất (committed pages).
  • data.db-wal: Tệp ghi nhật ký thay đổi tuần tự (Write-Ahead Log). Mọi thao tác INSERT, UPDATE, DELETE được append trực tiếp vào tệp này.
  • data.db-shm: Tệp bộ nhớ chia sẻ (Shared Memory Index). Hoạt động như một bảng chỉ mục trung gian ánh xạ bộ nhớ giữa các tiến trình đọc để xác định nhanh trang dữ liệu nằm trong data.db hay data.db-wal mà không gây ra xung đột I/O.
Luồng Đọc (Readers):  [Request 1..N] ──► Kiểm tra data.db-shm ──► Đọc trang mới nhất từ data.db-wal hoặc data.db (Non-blocking)

Luồng Ghi (Writer):   [Write Request] ──► Lấy RESERVED Lock ───► Ghi tuần tự vào data.db-wal ────────► Checkpoint đồng bộ về data.db

Ưu thế kỹ thuật vượt trội của WAL: 1. Readers không chặn Writer: Các luồng đọc có thể truy vấn đồng thời dữ liệu lịch sử trên data.db hoặc dữ liệu mới trên data.db-wal mà không làm ngắt quãng luồng đang ghi. 2. Writer không chặn Readers: Luồng ghi chỉ cần tuần tự append dữ liệu vào cuối tệp -wal, giải phóng hoàn toàn các luồng đọc khỏi trạng thái chờ khóa độc quyền. 3. Ghi tuần tự (Sequential Writes): Tệp -wal chỉ thực hiện các thao tác ghi nối tiếp thay vì ghi ngẫu nhiên (random write) phân tán trên toàn bộ tệp database chính, tận dụng tối đa băng thông I/O của ổ cứng.


Bộ thông số PRAGMA sống còn cho môi trường High-Concurrency

PocketBase mặc định đã cấu hình sẵn một số cờ WAL cơ bản, tuy nhiên để đạt ngưỡng throughput hàng chục nghìn truy vấn mỗi giây, kỹ sư vận hành cần kiểm tra và áp đặt bộ thông số PRAGMA tối ưu hóa sau đây vào cơ sở dữ liệu:

Tham số PRAGMA Giá trị tối ưu Ý nghĩa kỹ thuật và cơ chế vận hành
journal_mode WAL Kích hoạt cơ chế Write-Ahead Logging, cho phép đọc ghi đồng thời (concurrency).
synchronous NORMAL Thay vì FULL (ép fsync() sau mỗi write transaction), NORMAL chỉ gọi fsync() ở các mốc checkpoint critical. An toàn 100% trước lỗi sập ứng dụng (application crash), đảm bảo tính toàn vẹn ACID trong chế độ WAL trên hệ điều hành Linux. Tăng tốc độ ghi từ 5 đến 10 lần.
busy_timeout 5000 Thiết lập thời gian chờ tối đa 5000ms (5 giây) khi xuất hiện tranh chấp khóa ghi trước khi ném ngoại lệ SQLITE_BUSY. Ngăn ngừa lỗi drop request tức thì khi có transaction ghi kéo dài.
cache_size -64000 Cấp phát bộ nhớ đệm trang B-Tree. Dấu âm (-) biểu thị dung lượng tính theo Kibibyte. Giá trị -64000 tương đương ~64MB RAM được giữ lại cho cache trang tĩnh. Đối với node VPS có $\ge 8\text{GB}$ RAM, có thể nâng lên -128000 (128MB).
mmap_size 2147483648 Kích hoạt Memory-Mapped I/O với giới hạn 2GB (2 * 1024 * 1024 * 1024 bytes). Hệ điều hành ánh xạ tệp database trực tiếp vào không gian địa chỉ ảo của tiến trình, cho phép đọc dữ liệu qua con trỏ RAM, loại bỏ hoàn toàn chi phí context switch của system call read().
temp_store MEMORY Toàn bộ bảng tạm, con trỏ và chỉ mục trung gian (temporary tables/indexes) được lưu trữ hoàn toàn trên RAM thay vì tạo tệp rác trên disk.
wal_autocheckpoint 1000 Tự động kích hoạt cơ chế checkpoint thụ động khi tệp -wal tích lũy đủ 1000 trang (mặc định tương đương ~4MB dữ liệu).

Thẩm tra và thiết lập PRAGMA bằng công cụ SQLite CLI

Cài đặt tiện ích dòng lệnh sqlite3 trên máy chủ Ubuntu/Debian để tiến hành kiểm tra cấu hình tệp dữ liệu PocketBase:

sudo apt-get install -y sqlite3

Thực thi truy vấn thẩm tra trạng thái PRAGMA trực tiếp trên tệp data.db:

sudo sqlite3 /var/lib/pocketbase/pb_data/data.db <<EOF
PRAGMA journal_mode;
PRAGMA synchronous;
PRAGMA busy_timeout;
PRAGMA cache_size;
PRAGMA mmap_size;
PRAGMA temp_store;
EOF

Nếu hệ thống hiển thị các giá trị chưa đạt chuẩn tối ưu, tiến hành ghi đè cấu hình vĩnh viễn:

sudo sqlite3 /var/lib/pocketbase/pb_data/data.db <<EOF
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;
PRAGMA cache_size = -64000;
PRAGMA mmap_size = 2147483648;
PRAGMA temp_store = MEMORY;
VACUUM;
EOF

Thực hiện tương tự đối với tệp auxiliary.db (nơi lưu trữ nhật ký logs và request traces của PocketBase):

sudo sqlite3 /var/lib/pocketbase/pb_data/auxiliary.db <<EOF
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;
PRAGMA cache_size = -32000;
PRAGMA mmap_size = 1073741824;
PRAGMA temp_store = MEMORY;
EOF

Khởi động lại tiến trình PocketBase để đảm bảo connection pool của Go runtime nhận diện toàn bộ các thiết lập bộ nhớ chia sẻ mới:

sudo systemctl restart pocketbase

Kiểm soát hiện tượng WAL Bloat và chiến lược Checkpoint tự động

Một vấn đề nghiêm trọng thường gặp khi vận hành SQLite WAL dưới tải đọc cao là WAL Bloat (tệp -wal phình to bất thường lên tới hàng chục gigabyte). Hiện tượng này xảy ra khi có một kết nối đọc thực thi truy vấn quá lâu (long-running read query) hoặc tiến trình giữ active transaction không đóng. SQLite buộc phải giữ lại toàn bộ các phiên bản trang cũ trong -wal để duy trì tính cô lập Snapshot Isolation cho reader đó, khiến cơ chế tự động checkpoint (wal_autocheckpoint) bị hoãn lại vô thời hạn.

Hậu quả: Tệp -wal phình to làm suy giảm tốc độ đọc nghiêm trọng, do reader mới phải quét tuyến tính qua hàng triệu trang uncheckpointed trong WAL log trước khi tìm thấy dữ liệu.

Để ngăn chặn WAL Bloat, thiết lập một cronjob bảo trì hệ thống định kỳ mỗi đêm vào lúc 03:00 sáng. Script sẽ cưỡng chế checkpoint và thu nhỏ tệp nhật ký:

Tạo kịch bản bảo dưỡng tại /usr/local/bin/pocketbase-wal-checkpoint.sh:

#!/usr/bin/env bash
set -euo pipefail

DB_DIR="/var/lib/pocketbase/pb_data"
LOG_FILE="/var/log/pocketbase-maintenance.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "${LOG_FILE}"
}

log "Bắt đầu chu kỳ dọn dẹp và checkpoint SQLite WAL..."

# Kiểm tra dung lượng tệp WAL trước khi xử lý
if [[ -f "${DB_DIR}/data.db-wal" ]]; then
    WAL_SIZE_BEFORE=$(du -h "${DB_DIR}/data.db-wal" | cut -f1)
    log "Dung lượng data.db-wal hiện tại: ${WAL_SIZE_BEFORE}"
fi

# Thực thi PRAGMA wal_checkpoint(TRUNCATE)
# Chế độ TRUNCATE: Flush toàn bộ WAL pages vào database chính, sau đó cắt kích thước file -wal về 0 bytes.
sqlite3 "${DB_DIR}/data.db" "PRAGMA wal_checkpoint(TRUNCATE);" >> "${LOG_FILE}" 2>&1
sqlite3 "${DB_DIR}/auxiliary.db" "PRAGMA wal_checkpoint(TRUNCATE);" >> "${LOG_FILE}" 2>&1

# Tối ưu hóa lại cấu trúc B-Tree và giải phóng không gian phân mảnh định kỳ
sqlite3 "${DB_DIR}/data.db" "PRAGMA optimize;" >> "${LOG_FILE}" 2>&1

log "Hoàn tất chu kỳ checkpoint. Dung lượng tệp WAL đã được thu hẹp về 0 byte an toàn."

Phân quyền thực thi cho kịch bản:

sudo chmod +x /usr/local/bin/pocketbase-wal-checkpoint.sh

Đăng ký lịch chạy qua cron của tài khoản root:

echo "0 3 * * * /usr/local/bin/pocketbase-wal-checkpoint.sh >/dev/null 2>&1" | sudo tee -a /var/spool/cron/crontabs/root

Tối ưu hóa I/O Kernel Linux và cấu hình NVMe VPS

Dù SQLite được cấu hình tối ưu đến đâu, giới hạn cuối cùng của cơ chế ghi dữ liệu (đặc biệt là lệnh fsync() trong checkpoint) vẫn phụ thuộc hoàn toàn vào hệ thống con I/O của kernel và tốc độ vật lý của ổ đĩa.

Khi cài PocketBase trên VPS, nếu hạ tầng ảo hóa bị hiện tượng quá tải tranh chấp tài nguyên (CPU Steal Time %st tăng cao) hoặc sử dụng ổ cứng SSD SATA/HDD dùng chung (overcommitted shared storage), mỗi lời gọi fsync() có thể mất tới hàng chục mili-giây, làm nghẽn toàn bộ queue xử lý của PocketBase.

Để đáp ứng được khối lượng công việc này, môi trường triển khai thực tế trên hạ tầng đám mây KVM của tropic.host cung cấp thông số kỹ thuật lý tưởng: * Môi trường ảo hóa KVM thuần chủng với tài nguyên vCPU độc lập tuyệt đối, chỉ số CPU Steal Time %st = 0.0%, ngăn chặn việc tiến trình Go bị kernel tạm dừng (thread preemption) trong lúc đang giữ lock database. * Ổ cứng chuyên dụng chuẩn Enterprise NVMe SSD (PCIe 4.0), đạt tốc độ đọc ghi ngẫu nhiên 4K QD1 vượt ngưỡng 50,000 IOPS. Điều này đảm bảo các thao tác cập nhật trang B-Tree và lệnh fsync() hoàn tất ở mức dưới 80 microsecond ($\mu s$), triệt tiêu hoàn toàn độ trễ p99 đột biến.

Trên máy chủ VPS, tinh chỉnh các tham số xả trang bẩn (dirty memory pages) của nhân Linux bằng cách tạo tệp cấu hình sysctl:

sudo tee /etc/sysctl.d/99-sqlite-io.conf <<EOF
# Bắt đầu background flush dữ liệu bẩn xuống disk khi chạm 5% tổng RAM
vm.dirty_background_ratio = 5

# Buộc tiến trình ghi phải tạm dừng để xả đĩa khi trang bẩn đạt 10% tổng RAM (tránh tích tụ I/O spike)
vm.dirty_ratio = 10

# Giảm thời gian chờ trang bẩn trong RAM xuống 500 centisecs (5 giây)
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 100

# Tối ưu hóa bộ nhớ ảo, giảm thiểu hiện tượng swap đột ngột
vm.swappiness = 10
EOF

Áp dụng cấu hình kernel ngay lập tức mà không cần reboot:

sudo sysctl --system

Cuối cùng, kiểm tra I/O Scheduler của thiết bị lưu trữ block device chứa thư mục /var/lib/pocketbase/. Đối với các ổ đĩa NVMe chuẩn hiệu năng cao, thuật toán điều phối I/O phải được đặt ở chế độ none để nhân Linux chuyển giao quyền điều khiển hàng đợi trực tiếp cho bộ điều khiển phần cứng của ổ đĩa (hardware controller multi-queue), loại bỏ hoàn toàn chi phí tính toán lock ở tầng block layer:

cat /sys/block/nvme0n1/queue/scheduler

Đầu ra chuẩn xác phải phản ánh giá trị [none]:

[none] mq-deadline kyber bfq

Nếu hệ thống chưa chọn none, tiến hành gán lại và xác lập quy tắc udev:

echo "none" | sudo tee /sys/block/nvme0n1/queue/scheduler
echo 'ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"' | sudo tee /etc/udev/rules.d/60-nvme-scheduler.rules

Sau khi hoàn tất toàn bộ chuỗi tinh chỉnh từ kiến trúc SQLite WAL, tham số PRAGMA bộ nhớ đệm cho đến cấu hình hàng đợi I/O của kernel Linux, phiên bản PocketBase trên máy chủ VPS đã sẵn sàng duy trì hiệu suất đọc ghi ổn định, xử lý mượt mà hàng triệu bản ghi và đáp ứng thông lượng hàng nghìn kết nối đồng thời với độ trễ phản hồi cận microsecond.

Cấu Hình Reverse Proxy Caddy / Nginx Với SSL Tự Động

Sau khi tiến hành cài PocketBase trên VPS và tối ưu hóa hạ tầng lưu trữ tầng dưới, daemon PocketBase đang lắng nghe cục bộ tại địa chỉ loopback 127.0.0.1:8090. Mặc dù runtime Go tích hợp sẵn máy chủ HTTP có khả năng tự động cấp phát chứng chỉ Let's Encrypt qua cờ --dir và cổng 80/443, việc đưa trực tiếp tiến trình ứng dụng ra internet công cộng tiềm ẩn nhiều rủi ro về bảo mật, thiếu hụt các lớp đệm bảo vệ chống tấn công từ chối dịch vụ (slowloris, HTTP flood) và hạn chế khả năng kiểm soát giao thức truyền tải nâng cao.

Triển khai một lớp reverse proxy chuyên dụng như Caddy hoặc Nginx phía trước PocketBase là tiêu chuẩn kỹ thuật bắt buộc để thiết lập cơ chế SSL/TLS tự động, kiểm soát kích thước payload upload tệp nhị phân, tối ưu hóa giao thức HTTP/2, HTTP/3 (QUIC) và đặc biệt là duy trì đường truyền Server-Sent Events (SSE) cho tính năng realtime subscription không bị gián đoạn.

                    ┌────────────────────────────────────────────────────────┐
                    │               Internet Client (HTTPS/WSS)               │
                    └───────────────────────────┬────────────────────────────┘
                                                │ Port 443 (TCP/UDP HTTP/3)
                                                ▼
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ KVM VPS (tropic.host - BGP Peering / Dedicated IPv4 / TCP BBR)                             │
│                                                                                            │
│   ┌────────────────────────────────────────────────────────────────────────────────────┐   │
│   │ Reverse Proxy: Caddy 2.x hoặc Nginx 1.26+                                          │   │
│   │ ├── TLS Termination (TLS 1.3, OCSP Stapling, HSTS)                                 │   │
│   │ ├── Anti-Buffering Engine (`X-Accel-Buffering: no`, `flush_interval -1`)           │   │
│   │ └── Body Limit: 50MB (PocketBase File Storage)                                     │   │
│   └─────────────────────────────────────────┬──────────────────────────────────────────┘   │
│                                             │ Unix Socket / TCP Loopback                   │
│                                             │ http://127.0.0.1:8090                        │
│                                             ▼                                              │
│   ┌────────────────────────────────────────────────────────────────────────────────────┐   │
│   │ PocketBase Daemon (Go / SQLite WAL)                                                │   │
│   │ ├── REST API Engine (/api/collections/...)                                         │   │
│   │ ├── Realtime Subscriptions (/api/realtime - Server-Sent Events)                    │   │
│   │ └── Admin UI Dashboard (/_/)                                                       │   │
│   └────────────────────────────────────────────────────────────────────────────────────┘   │
└────────────────────────────────────────────────────────────────────────────────────────────┘

1. Chuẩn Bị Tường Lửa Và Hạ Tầng Mạng

Trước khi thiết lập web server, cần trỏ bản ghi DNS A (hoặc AAAA cho IPv6) của tên miền định tuyến trực tiếp về địa chỉ IP tĩnh chuyên dụng của máy chủ. Nền tảng cloud KVM tại tropic.host cung cấp địa chỉ IPv4 sạch, không bị gắn cờ blacklist bởi Spamhaus hay các tổ chức an ninh mạng quốc tế, giúp quá trình ACME challenge xác thực tên miền diễn ra tức thì mà không gặp tình trạng timeout gói tin.

Cấu hình tường lửa nftables hoặc tiện ích ufw để mở các cổng mạng tiêu chuẩn:

# Cho phép lưu lượng HTTP (Port 80) phục vụ quá trình HTTP-01 ACME Challenge
sudo ufw allow 80/tcp comment "HTTP ACME Challenge"

# Cho phép lưu lượng HTTPS (Port 443) trên cả hai giao thức TCP và UDP (HTTP/3 QUIC)
sudo ufw allow 443/tcp comment "HTTPS Standard"
sudo ufw allow 443/udp comment "HTTP/3 QUIC Protocol"

# Kích hoạt và kiểm tra trạng thái
sudo ufw reload
sudo ufw status verbose

2. Phương Án 1: Triển Khai Caddy Với HTTP/3 Tự Động (Khuyến Nghị)

Caddy là giải pháp reverse proxy lý tưởng nhất cho hệ sinh thái Go nhờ kiến trúc hiện đại, khả năng tự động quản lý vòng đời chứng chỉ số (Let's Encrypt / ZeroSSL) mà không cần cronjob ngoài, đồng thời mặc định kích hoạt giao thức HTTP/3 qua giao vận UDP.

Cài đặt Caddy trên Ubuntu/Debian

sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLF 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLF 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy

Cấu hình /etc/caddy/Caddyfile

Mở tệp cấu hình chính và định nghĩa cụm máy chủ ảo. Cần lưu ý xử lý đường dẫn /api/realtime: PocketBase sử dụng kiến trúc Server-Sent Events (SSE) để truyền dữ liệu thời gian thực. Caddy cần thiết lập flush_interval -1 để chuyển tiếp ngay lập tức từng chunk dữ liệu đến client mà không lưu vào bộ đệm nội bộ:

pb.example.com {
    # Giới hạn kích thước tải lên cho các tập tin đính kèm vào collection
    request_body {
        max_size 50MB
    }

    # Bật nén luồng tĩnh bằng Zstandard và Gzip để tiết kiệm băng thông
    encode zstd gzip

    # Xử lý các tiêu đề bảo mật HTTP Security Headers
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    # Định tuyến toàn bộ lưu lượng tới backend PocketBase
    reverse_proxy 127.0.0.1:8090 {
        # Tắt bộ đệm phản hồi để đảm bảo luồng SSE hoạt động tức thì với độ trễ thấp
        flush_interval -1

        # Chuyển tiếp các trường tiêu đề thực tế của client
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Giữ kết nối TCP upstream mở lâu hơn nhằm giảm chi phí bắt tay TLS/TCP
        transport http {
            keepalive 300s
            keepalive_idle_conns 128
        }
    }

    # Ghi log chuẩn định dạng JSON phục vụ phân tích hệ thống
    log {
        output file /var/log/caddy/pocketbase_access.log {
            roll_size 20MB
            roll_keep 10
            roll_keep_for 720h
        }
        format json
    }
}

Kiểm tra cú pháp và kích hoạt dịch vụ:

# Kiểm tra tính hợp lệ của Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile

# Khởi động lại và cấu hình tự chạy cùng hệ điều hành
sudo systemctl restart caddy
sudo systemctl enable caddy

3. Phương Án 2: Triển Khai Nginx Hardening & Certbot (Tiêu Chuẩn Doanh Nghiệp)

Nếu hạ tầng yêu cầu tích hợp Nginx đồng bộ với các dịch vụ khác trên máy chủ, cấu hình cần được tinh chỉnh kỹ lưỡng về buffer, timeout và module SSL.

Cài đặt Nginx và Certbot

sudo apt-get update
sudo apt-get install -y nginx certbot python3-certbot-nginx

Thiết lập Virtual Host /etc/nginx/sites-available/pocketbase.conf

Tạo tệp cấu hình mới nhằm xử lý triệt để bài toán ngắt kết nối SSE và tối ưu hóa tải cho các tệp tĩnh do PocketBase phục vụ:

# Map cấu hình nâng cấp giao thức nếu client sử dụng WebSocket
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name pb.example.com;

    # Chuyển hướng toàn bộ lưu lượng HTTP sang HTTPS với mã 301
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name pb.example.com;

    # Đường dẫn chứng chỉ SSL do Certbot quản lý
    ssl_certificate /etc/letsencrypt/live/pb.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pb.example.com/privkey.pem;

    # Cấu hình mã hóa bảo mật cao (chỉ cho phép TLS 1.2 và TLS 1.3)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # Tối ưu hóa bộ nhớ đệm phiên SSL (SSL Session Cache)
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Kích hoạt OCSP Stapling để xác thực chứng chỉ nhanh chóng
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/pb.example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # HTTP Security Headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Giới hạn kích thước tải lên tối đa của body (phục vụ upload media vào PocketBase)
    client_max_body_size 50M;

    # Tối ưu hóa kích thước bộ đệm tiêu đề
    client_body_buffer_size 128k;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 16k;

    # Cấu hình proxy ngược đến PocketBase backend
    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_http_version 1.1;

        # Thiết lập các trường tiêu đề kết nối
        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 Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # =========================================================================
        # CẤU HÌNH ĐẶC TẢ CHO SERVER-SENT EVENTS (SSE) & STREAMING
        # =========================================================================
        # Vô hiệu hóa bộ đệm trung gian để các sự kiện /api/realtime được đẩy lập tức
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding off;

        # Thiết lập timeout kéo dài để ngăn Nginx tự động đóng kết nối idle của client
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_connect_timeout 60s;

        # Bảo vệ chống tràn TCP socket
        tcp_nodelay on;
    }

    # Ghi log chi tiết
    access_log /var/log/nginx/pocketbase_access.log;
    error_log /var/log/nginx/pocketbase_error.log warn;
}

Kích hoạt cấu hình và cấp phát chứng chỉ SSL bằng Certbot:

# Tạo thư mục webroot cho certbot
sudo mkdir -p /var/www/certbot

# Tạo liên kết tượng trưng (symlink)
sudo ln -sf /etc/nginx/sites-available/pocketbase.conf /etc/nginx/sites-enabled/

# Tạm thời comment khối SSL để khởi chạy port 80 xin chứng chỉ lần đầu
sudo certbot certonly --webroot -w /var/www/certbot -d pb.example.com --non-interactive --agree-tos -m [email protected]

# Kiểm tra cú pháp Nginx
sudo nginx -t

# Tải lại cấu hình Nginx
sudo systemctl reload nginx

4. Tinh Chỉnh Tham Số Socket Mạng Ở Tầng Kernel Linux

Duy trì hàng chục nghìn kết nối đồng thời (realtime subscribers) thông qua reverse proxy đòi hỏi nhân Linux phải được nới lỏng giới hạn về số lượng file descriptor và vùng đệm socket mạng. Áp dụng các tham số sau vào /etc/sysctl.d/99-network-proxy.conf:

# Mở rộng dải cổng cục bộ cho các kết nối upstream loopback
net.ipv4.ip_local_port_range = 1024 65535

# Tái sử dụng socket ở trạng thái TIME_WAIT
net.ipv4.tcp_tw_reuse = 1

# Mở rộng hàng đợi kết nối đang chờ xử lý
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 16384

# Tối ưu hóa kích thước bộ đệm socket đọc/ghi
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# Thời gian duy trì kết nối keepalive thăm dò
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5

Thực thi áp dụng ngay lập tức:

sudo sysctl --system

Nhờ thuật toán điều khiển tắc nghẽn TCP BBR được kích hoạt mặc định trên nền tảng KVM VPS của tropic.host, kết hợp cùng hạ tầng cổng mạng uplink từ 1 đến 10 Gbps và kết nối trực tiếp tại các điểm trung chuyển Internet lớn (Frankfurt, Amsterdam, Singapore), độ biến thiên trễ (jitter) và tình trạng rớt gói tin trên các kênh SSE kéo dài được triệt tiêu hoàn toàn.


5. Kiểm Tra Tính Toàn Vẹn Và Đo Lường Hiệu Năng Phản Hồi

Sau khi kích hoạt reverse proxy, tiến hành kiểm tra mã trạng thái, chứng chỉ SSL và luồng dữ liệu thời gian thực từ máy trạm:

# Kiểm tra phản hồi HTTP/2 và chứng chỉ TLS 1.3
curl -Iv https://pb.example.com/api/health

# Kiểm tra luồng Server-Sent Events không bị lưu đệm (bắt buộc nhận header text/event-stream)
curl -N -H "Accept: text/event-stream" https://pb.example.com/api/realtime

Đầu ra của lệnh kiểm tra SSE phải xuất hiện phản hồi xác nhận kết nối và không bị treo ở bộ đệm của reverse proxy:

HTTP/2 200 
content-type: text/event-stream; charset=UTF-8
cache-control: no-cache
x-accel-buffering: no
date: Sun, 04 Oct 2026 17:05:12 GMT

data: {"clientId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}

Sự hiện diện của trường content-type: text/event-stream và việc nhận ngay lập tức chuỗi clientId minh chứng rằng kiến trúc reverse proxy đã loại bỏ hoàn toàn các lớp đệm cản trở, đảm bảo PocketBase trên VPS sẵn sàng phục vụ các ứng dụng mobile/web với tính năng đồng bộ thời gian thực đạt độ tin cậy tuyệt đối.

Quản Trị Người Dùng, Phân Quyền API Rules và Upload Tệp An Toàn

Sau khi thiết lập reverse proxy hoàn tất và kiểm tra thông suốt luồng SSE, hệ thống đã sẵn sàng tiếp nhận các kết nối ứng dụng thực tế. Tuy nhiên, khi cài PocketBase trên VPS cho môi trường production, rủi ro rò rỉ dữ liệu hoặc chiếm quyền kiểm soát máy chủ phần lớn không xuất phát từ lỗ hổng nhị phân của Go, mà nằm ở việc cấu hình sai lệch API Rules và thiếu kiểm soát các luồng tải lên tệp tin (File Uploads).

PocketBase tiếp cận việc quản trị dữ liệu thông qua cơ chế Collection-driven, tích hợp sẵn hệ thống xác thực (Auth Collection) và bảo mật mức bản ghi (Row-Level Security) mà không cần viết mã backend trung gian.


1. Kiến Trúc Auth Collections và Cơ Chế Token JWT

Mỗi tài khoản người dùng trong PocketBase được quản lý trong một Auth Collection (mặc định là users, có thể khởi tạo thêm các bảng auth riêng biệt như clients, staff). Hệ thống cấp phát JSON Web Token (JWT) theo thuật ngữ chuẩn RFC 7519, ký bằng thuật toán HMAC-SHA256 (HS256).

Chu kỳ vòng đời mặc định của User Token là 14 ngày (1,209,600 giây). Trong các ứng dụng yêu cầu bảo mật cao, cần rút ngắn thời gian này xuống mức 3600–86400 giây và kích hoạt xác thực lại để giảm thiểu nguy cơ khi token bị đánh cắp.

Để điều chỉnh trực tiếp thông số này thông qua REST API hoặc trong tệp cấu hình của PocketBase:

# Cập nhật thời hạn token của bảng 'users' về mức 24 giờ (86400 giây) qua API nội bộ
curl -X PATCH http://127.0.0.1:8090/api/collections/users \
  -H "Authorization: Admin <ADMIN_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "authRule": "verified = true",
    "authAlert": {"enabled": true},
    "options": {
      "manageRule": null,
      "allowOAuth2Auth": true,
      "allowUsernameAuth": true,
      "allowEmailAuth": true,
      "requireEmail": true,
      "token": {
        "duration": 86400
      }
    }
  }'

Thiết lập "authRule": "verified = true" đảm bảo rằng người dùng chỉ có thể thực hiện xác thực và nhận JWT hợp lệ sau khi đã nhấp vào liên kết xác nhận địa chỉ email, ngăn chặn việc đăng ký tài khoản rác hàng loạt làm cạn kiệt dung lượng cơ sở dữ liệu.


2. Thiết Lập API Rules: Kiểm Soát Phân Quyền Mức Bản Ghi (Row-Level Access)

API Rules trong PocketBase hoạt động tương tự bộ lọc SQL WHERE, được áp dụng tự động cho 5 hành vi dữ liệu: List/Search, View, Create, Update, và Delete.

Một quy tắc có 3 trạng thái cốt lõi: * null (Locked): Chỉ Admin (Superuser) mới có quyền thực thi. Người dùng thông thường hoặc khách vãng lai sẽ nhận mã phản hồi 403 Forbidden. * "" (Empty string): Công khai hoàn toàn (Public). Bất kỳ client nào gửi request không kèm token đều có quyền truy cập. * Biểu thức điều kiện (Expression): Thực thi đánh giá logic dựa trên context của request (@request.auth.*, @request.data.*) và các trường dữ liệu của bản ghi hiện tại.

Giả sử hệ thống triển khai một bảng lưu trữ tài liệu người dùng có tên documents, bao gồm các trường: title (text), file (file), owner (relation tới users), is_public (bool), role_allowed (select: editor, viewer).

Cấu hình phân quyền bảo mật cấp độ doanh nghiệp cho bảng documents cần tuân thủ cấu trúc sau:

# 1. List/Search Rule:
# Cho phép xem nếu là tài liệu công khai HOẶC người dùng hiện tại là chủ sở hữu
@request.auth.id != "" && (is_public = true || owner = @request.auth.id)

# 2. View Rule:
# Đồng nhất với quy tắc List để tránh tình trạng ID enumeration
@request.auth.id != "" && (is_public = true || owner = @request.auth.id)

# 3. Create Rule:
# Bắt buộc người dùng phải đăng nhập và trường owner gửi lên phải trùng với ID trong JWT
@request.auth.id != "" && @request.data.owner = @request.auth.id

# 4. Update Rule:
# Chỉ chủ sở hữu được sửa, VÀ ngăn chặn việc tự ý chuyển quyền sở hữu tài liệu sang người khác
@request.auth.id != "" && owner = @request.auth.id && @request.data.owner:isset = false

# 5. Delete Rule:
# Chỉ chủ sở hữu bản ghi có quyền xóa
@request.auth.id != "" && owner = @request.auth.id

Cú pháp @request.data.owner:isset = false là chốt chặn quan trọng nhằm triệt tiêu lỗ hổng leo thang đặc quyền (Privilege Escalation): nó ngăn không cho client gửi payload chứa trường owner nhằm ghi đè giá trị khóa ngoại trong cơ sở dữ liệu.


3. Lưu Trữ Tệp Cục Bộ (Local Storage) và Gia Cố Tầng Filesystem

Mặc định, khi cài PocketBase trên VPS, toàn bộ tệp tin tải lên được đặt tại thư mục pb_data/storage/<collection_id>/<record_id>/<file_name>. Tệp tin được cấp tên ngẫu nhiên chứa chuỗi hash để chống tấn công dò đoán URL (Predictable Resource Location).

Hạn Chế MIME Type và File Size tại Cấp Độ Collection

Để triệt tiêu nguy cơ tải lên mã độc thực thi (Remote Code Execution - RCE), tuyệt đối không để trường file ở dạng mở. Trong Schema của Collection, bắt buộc khai báo giới hạn MIME types:

  • Tài liệu hình ảnh: Chỉ chấp nhận image/jpeg, image/png, image/webp. Ngăn chặn hoàn toàn image/svg+xml nếu không có bộ lọc khử khuẩn SVG (SVG sanitization), do tệp SVG có thể chứa mã JavaScript độc hại gây tấn công Stored XSS.
  • Kích thước tối đa: Giới hạn tệp không vượt quá nhu cầu thực tế (ví dụ: 5242880 bytes tương đương 5MB cho ảnh, 20971520 bytes cho tài liệu PDF).

Phân Quyền Filesystem Cho Người Dùng Service

Đảm bảo thư mục lưu trữ cục bộ không thể bị ghi đè bởi các tiến trình khác trên hệ điều hành và ngăn cản thực thi binary trực tiếp từ thư mục upload:

# Phân quyền nghiêm ngặt cho tiến trình pocketbase
sudo chown -R pocketbase:pocketbase /opt/pocketbase/pb_data
sudo chmod -R 750 /opt/pocketbase/pb_data

# Đảm bảo các tệp mới sinh ra kế thừa đúng quyền
sudo chmod g+s /opt/pocketbase/pb_data/storage

Khi vận hành trên nền tảng đám mây tropic.host, việc lưu trữ tệp tin trực tiếp trên ổ cứng cục bộ tận dụng tối đa băng thông của ổ NVMe PCIe 4.0 với tốc độ đọc ngẫu nhiên 4K QD1 vượt trên 50.000 IOPS. Lợi thế này giúp PocketBase tạo ảnh thu nhỏ (thumbnails) tức thì theo thời gian thực mà không làm tăng tải I/O của CPU (giữ chỉ số %iowait tiệm cận mức 0.0%).


4. Tích Hợp S3-Compatible Object Storage Cho Hạ Tầng Phân Tán

Khi quy mô lưu trữ vượt quá giới hạn ổ cứng của một node VPS đơn lẻ hoặc khi xây dựng mô hình sao lưu phi tập trung, PocketBase cho phép chuyển hướng toàn bộ luồng nhị phân sang các hệ thống lưu trữ tương thích S3 (MinIO, Cloudflare R2, AWS S3, Wasabi).

Khi kích hoạt S3, PocketBase chỉ đóng vai trò xác thực quyền (API Rules). Nếu client đủ điều kiện tải tệp, PocketBase sẽ thực hiện ký URL có thời hạn (Presigned URL) hoặc làm proxy chuyển tiếp dữ liệu tùy theo cấu hình.

Cấu hình tích hợp S3 được áp dụng thông qua API /api/settings:

curl -X PATCH http://127.0.0.1:8090/api/settings \
  -H "Authorization: Admin <ADMIN_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "s3": {
      "enabled": true,
      "bucket": "production-pb-data",
      "region": "eu-central-1",
      "endpoint": "https://s3.eu-central-1.amazonaws.com",
      "accessKey": "AKIAIOSFODNN7EXAMPLE",
      "secret": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
      "s3ForcePathStyle": false
    }
  }'

Lưu ý đối với hạ tầng tự host MinIO hoặc Cloudflare R2: Nếu sử dụng MinIO nội bộ, trường s3ForcePathStyle bắt buộc phải đặt thành true để PocketBase cấu trúc đường dẫn dạng endpoint/bucket/file thay vì bucket.endpoint/file.

Nhờ hạ tầng mạng 1–10 Gbps của tropic.host với các kết nối trực tiếp tại các điểm trung chuyển Internet lớn (DE-CIX Frankfurt, AMS-IX Amsterdam), độ trễ truyền dữ liệu từ VPS tới các cụm S3 nội lục châu Âu được duy trì ở mức dưới 5ms, triệt tiêu hiện tượng thắt nút cổ chai mạng khi xử lý các luồng tải tệp đa luồng.


5. Kiểm Thử Phân Quyền và Tải Tệp Thực Tế Bằng Terminal

Để xác nhận quy tắc bảo mật hoạt động chuẩn xác theo logic định sẵn, thực hiện kịch bản kiểm tra tự động bằng lệnh curl:

Bước 1: Tạo Người Dùng Thử Nghiệm và Nhận Token

# Đăng ký tài khoản
curl -s -X POST https://pb.example.com/api/collections/users/records \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"SecurePassword123!","passwordConfirm":"SecurePassword123!"}'

# Xác thực nhận JWT
USER_TOKEN=$(curl -s -X POST https://pb.example.com/api/collections/users/auth-with-password \
  -H "Content-Type: application/json" \
  -d '{"identity":"[email protected]","password":"SecurePassword123!"}' | jq -r '.token')

USER_ID=$(curl -s -X POST https://pb.example.com/api/collections/users/auth-with-password \
  -H "Content-Type: application/json" \
  -d '{"identity":"[email protected]","password":"SecurePassword123!"}' | jq -r '.record.id')

Bước 2: Tải Tệp Tin Đính Kèm Bản Ghi Lên Hệ Thống

# Tạo một tệp ảnh mẫu
dd if=/dev/urandom of=/tmp/avatar.png bs=1M count=1

# Gửi request dạng multipart/form-data có kèm token xác thực
curl -i -X POST https://pb.example.com/api/collections/documents/records \
  -H "Authorization: Bearer $USER_TOKEN" \
  -F "title=Network Architecture Diagram" \
  -F "owner=$USER_ID" \
  -F "is_public=false" \
  -F "file=@/tmp/avatar.png;type=image/png"

Đầu ra trả về mã HTTP 200 OK hoặc 201 Created kèm theo cấu trúc JSON của bản ghi và tên file đã được hash:

{
  "id": "abc123xyz456",
  "collectionId": "col_doc_id",
  "title": "Network Architecture Diagram",
  "owner": "usr_99887766",
  "is_public": false,
  "file": "avatar_a1b2c3d4e5.png"
}

Bước 3: Kiểm Tra Tính Chặt Chẽ Của API Rule Đối Với Khách Vãng Lai

Thực hiện truy vấn bản ghi vừa tạo mà không truyền Header Authorization:

curl -i -X GET https://pb.example.com/api/collections/documents/records/abc123xyz456

Phản hồi từ PocketBase bắt buộc phải là 404 Not Found (PocketBase chủ động trả về 404 thay vì 403 đối với các bản ghi không thỏa mãn View Rule để ngăn chặn kẻ tấn công quét ID):

HTTP/2 404
content-type: application/json; charset=UTF-8
date: Sun, 04 Oct 2026 17:15:30 GMT

{
  "code": 404,
  "message": "The requested resource wasn't found.",
  "data": {}
}

Kiến trúc này đảm bảo dữ liệu nhạy cảm được cô lập tuyệt đối ở tầng nhân ứng dụng, biến PocketBase thành một backend tin cậy, an toàn và sẵn sàng cho các kịch bản chịu tải lớn mà không đòi hỏi các lớp mã trung gian phức tạp.

Quy Trình Tự Động Sao Lưu SQLite và Khôi Phục Nhanh Chóng

1. Bản Chất Giao Dịch SQLite WAL và Nguy Cơ Corrupt Dữ Liệu Khi Sao Lưu Thô

Khi triển khai giải pháp cài pocketbase trên vps ở môi trường production, PocketBase kích hoạt cơ chế ghi nhật ký trước giao dịch (Write-Ahead Logging - WAL) trên SQLite. Trạng thái cơ sở dữ liệu phân tán trên ba file chính: * data.db: Không gian lưu trữ các B-Tree page chính. * data.db-wal: Chứa các transaction commit mới nhất chưa được checkpoint đồng bộ ngược vào file chính. * data.db-shm: Bộ nhớ chia sẻ (shared memory file) quản lý chỉ mục đồng thời cho các tiến trình đọc/ghi.

Thao tác sao lưu cơ học bằng các lệnh tầng file hệ thống như cp /pb_data/data.db /backup/ hoặc đóng gói trực tiếp bằng tar khi PocketBase đang chạy là một sai lầm nghiêm trọng. Nếu tiến trình ghi xảy ra đồng thời, việc sao chép bất đối xứng giữa data.db và data.db-wal sẽ dẫn đến hiện tượng rách trang (torn pages), phá vỡ liên kết B-Tree và khiến cơ sở dữ liệu rơi vào trạng thái hỏng hóc vĩnh viễn (database corruption).

Giải pháp kỹ thuật tiêu chuẩn là sử dụng SQLite Online Backup API thông qua cú pháp .backup hoặc câu lệnh VACUUM INTO. Phương thức này thiết lập một shared read lock nội bộ ở mức tối thiểu mà không chặn đứng các truy vấn đọc khác, tuần tự sao chép từng database page ra một file đích độc lập với trạng thái giao dịch nhất quán (point-in-time consistency), sau đó tự động giải phóng lock mà không ảnh hưởng tới tiến trình PocketBase đang phục vụ client.

2. Thiết Lập Công Cụ và Kênh Đồng Bộ Offsite (Rclone & S3)

Toàn bộ quy trình sao lưu phải tuân thủ nghiêm ngặt nguyên tắc dự phòng 3-2-1: giữ bản sao trên ổ đĩa cục bộ, chuyển bản sao nén ra ngoài hệ thống (offsite) qua lưu trữ đám mây tương thích S3 (Cloudflare R2, AWS S3, MinIO, Wasabi).

Cài đặt các gói nhị phân cần thiết trên máy chủ Ubuntu/Debian:

apt-get update && apt-get install -y sqlite3 zstd rclone curl ca-certificates

Khởi tạo cấu hình kết nối Rclone tới S3 bucket lưu trữ offsite tại /root/.config/rclone/rclone.conf:

[remote-s3]
type = s3
provider = Cloudflare
access_key_id = 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
secret_access_key = f1e2d3c4b5a697887766554433221100aabbccddeeff
endpoint = https://<account_id>.r2.cloudflarestorage.com
acl = private

Kiểm tra kết nối và tính sẵn sàng của bucket:

rclone lsd remote-s3:

3. Triển Khai Kịch Bản Sao Lưu Tự Động Đạt Chuẩn Production

Tạo script thực thi sao lưu toàn diện tại /usr/local/bin/pocketbase-backup.sh. Script đảm nhiệm việc: 1. Đảm bảo tính độc quyền thông qua file lock (flock) nhằm ngăn hai tiến trình backup chạy trùng lặp. 2. Sao lưu an toàn hai cơ sở dữ liệu: data.db (dữ liệu ứng dụng) và auxiliary.db (nhật ký truy vấn, telemetry). 3. Sao lưu thư mục pb_data/storage (file attachment, ảnh, tài liệu của người dùng). 4. Nén song song đa luồng bằng thuật toán Zstandard (zstd) với mức nén cao. 5. Tạo mã băm SHA256 để xác thực tính toàn vẹn. 6. Đẩy dữ liệu ra bucket S3 và dọn dẹp các bản sao lưu cục bộ cũ hơn 7 ngày.

#!/usr/bin/env bash
set -Eeuo pipefail

# ==============================================================================
# POCKETBASE AUTOMATED BACKUP ENGINE
# ==============================================================================
PB_DIR="/var/lib/pocketbase"
PB_DATA="${PB_DIR}/pb_data"
BACKUP_TMP_DIR="/tmp/pb_backup_staging"
BACKUP_LOCAL_DIR="/var/backups/pocketbase"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_NAME="pb_backup_${TIMESTAMP}"
DEST_TAR="${BACKUP_LOCAL_DIR}/${BACKUP_NAME}.tar.zst"
REMOTE_BUCKET="remote-s3:prod-backups/pocketbase"
LOCK_FILE="/var/lock/pocketbase-backup.lock"
RETENTION_DAYS=7

# Cơ chế chống Race Condition bằng file lock
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "[ERROR] Backup process is already running." >&2; exit 1; }

echo "[+] [$(date)] Bắt đầu tiến trình sao lưu PocketBase..."

mkdir -p "$BACKUP_TMP_DIR" "$BACKUP_LOCAL_DIR"

cleanup() {
    rm -rf "$BACKUP_TMP_DIR"
}
trap cleanup EXIT INT TERM

# Bước 1: Sao lưu an toàn bằng SQLite Online Backup API
echo "[+] Sao lưu SQLite data.db và auxiliary.db..."
sqlite3 "${PB_DATA}/data.db" ".backup '${BACKUP_TMP_DIR}/data.db'"
if [[ -f "${PB_DATA}/auxiliary.db" ]]; then
    sqlite3 "${PB_DATA}/auxiliary.db" ".backup '${BACKUP_TMP_DIR}/auxiliary.db'"
fi

# Bước 2: Đồng bộ cấu trúc thư mục lưu trữ file người dùng
echo "[+] Sao chép thư mục lưu trữ storage..."
if [[ -d "${PB_DATA}/storage" ]]; then
    cp -a "${PB_DATA}/storage" "${BACKUP_TMP_DIR}/storage"
fi

# Bước 3: Đóng gói và nén bằng zstd đa luồng
echo "[+] Đóng gói và nén dữ liệu qua zstd..."
tar -C "$BACKUP_TMP_DIR" -cf - . | zstd -T0 -19 -o "$DEST_TAR"

# Bước 4: Tạo mã kiểm tra băm SHA256
echo "[+] Sinh mã checksum SHA256..."
sha256sum "$DEST_TAR" > "${DEST_TAR}.sha256"

# Bước 5: Đồng bộ offsite lên S3 qua Rclone
echo "[+] Đẩy dữ liệu ra hạ tầng lưu trữ S3 offsite..."
rclone copy "$DEST_TAR" "$REMOTE_BUCKET" --fast-list --s3-upload-concurrency 8
rclone copy "${DEST_TAR}.sha256" "$REMOTE_BUCKET"

# Bước 6: Xoá bản sao lưu cục bộ cũ
echo "[+] Xóa các bản sao lưu cục bộ cũ hơn ${RETENTION_DAYS} ngày..."
find "$BACKUP_LOCAL_DIR" -type f -name "pb_backup_*.tar.zst*" -mtime "+${RETENTION_DAYS}" -delete

echo "[✓] [$(date)] Sao lưu hoàn tất thành công: ${DEST_TAR}"

Phân quyền chặt chẽ cho script:

chmod 700 /usr/local/bin/pocketbase-backup.sh
chown root:root /usr/local/bin/pocketbase-backup.sh

4. Quản Trị Tác Vụ Bằng Systemd Timer và Cô Lập Tài Nguyên (cgroups)

Thay vì sử dụng Cron truyền thống thiếu cơ chế giám sát log tập trung, ta thiết lập systemd.timer kết hợp systemd.service nhằm tận dụng journalctl và giới hạn tài nguyên qua cgroups, tránh tình trạng tiến trình nén file chiếm dụng CPU gây ảnh hưởng đến PocketBase.

Tạo service unit /etc/systemd/system/pocketbase-backup.service:

[Unit]
Description=PocketBase Automated Backup Service
After=network-online.target local-fs.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
Group=root
ExecStart=/usr/local/bin/pocketbase-backup.sh

# Cấu hình kiểm soát và cô lập tài nguyên qua cgroups v2
Nice=19
CPUSchedulingPolicy=idle
CPUWeight=100
MemoryHigh=512M
MemoryMax=768M

# Gia cố bảo mật hệ thống tệp
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/backups/pocketbase /tmp /var/lock /root/.config/rclone
PrivateTmp=true
NoNewPrivileges=true

Tạo timer unit /etc/systemd/system/pocketbase-backup.timer thiết lập chu kỳ sao lưu mỗi 6 tiếng một lần:

[Unit]
Description=Trigger PocketBase Backup Every 6 Hours
RefuseManualStart=no
RefuseManualStop=no

[Timer]
OnCalendar=*-*-* 00,06,12,18:00:00
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target

Kích hoạt và nạp cấu hình timer:

systemctl daemon-reload
systemctl enable --now pocketbase-backup.timer

Kiểm tra trạng thái kích hoạt của timer:

systemctl list-timers pocketbase-backup.timer

5. Quy Trình Khôi Phục Dữ Liệu Sau Thảm Họa (Disaster Recovery Runbook)

Sao lưu sẽ vô nghĩa nếu không có quy trình khôi phục được kiểm thử thực tế. Khi xảy ra sự cố sập ổ cứng hoặc dữ liệu bị phá hủy, kỹ sư vận hành thực hiện chính xác quy trình khôi phục theo 6 bước dưới đây:

Bước 1: Dừng Ngay Lập Tức Dịch Vụ Ứng Dụng

Ngăn chặn các truy vấn ghi tiếp tục phát sinh nhằm cô lập trạng thái hệ thống:

systemctl stop pocketbase.service

Bước 2: Tải Bản Sao Lưu Mới Nhất Từ S3

Liệt kê danh sách các file nén trên bucket và kéo bản ghi mới nhất về thư mục cứu hộ:

LATEST_BACKUP=$(rclone lsf remote-s3:prod-backups/pocketbase/ | grep '\.tar\.zst$' | sort | tail -n 1)
mkdir -p /tmp/pb_restore
rclone copy "remote-s3:prod-backups/pocketbase/${LATEST_BACKUP}" /tmp/pb_restore/
rclone copy "remote-s3:prod-backups/pocketbase/${LATEST_BACKUP}.sha256" /tmp/pb_restore/

Bước 3: Xác Thực Toàn Vẹn Bằng Checksum SHA256

Kiểm tra tính toàn vẹn của file nén trước khi bung dữ liệu. Tuyệt đối không phục hồi từ file có mã hash sai lệch:

cd /tmp/pb_restore
sha256sum -c "${LATEST_BACKUP}.sha256"

Đầu ra bắt buộc phải trả về: ${LATEST_BACKUP}: OK.

Bước 4: Giải Nén và Kiểm Tra Tính Toàn Vẹn SQLite (Integrity Check)

Giải nén dữ liệu vào vùng đệm tạm thời:

mkdir -p /tmp/pb_restore/unpacked
tar --zstd -xf "${LATEST_BACKUP}" -C /tmp/pb_restore/unpacked

Chạy kiểm tra chuyên sâu B-Tree và khóa ngoại (Foreign Keys) của SQLite bằng PRAGMA:

sqlite3 /tmp/pb_restore/unpacked/data.db "PRAGMA integrity_check;"
sqlite3 /tmp/pb_restore/unpacked/data.db "PRAGMA foreign_key_check;"

Nếu kết quả trả về là ok và không có lỗi ràng buộc khóa ngoại, cơ sở dữ liệu hoàn toàn an toàn để đưa vào hoạt động.

Bước 5: Đưa Dữ Liệu Vào Vị Trí Vận Hành và Chuẩn Hóa Quyền Hạn

Sao lưu lại thư mục dữ liệu hỏng hiện tại để đối soát forensics, sau đó tráo đổi dữ liệu sạch vào vị trí chính:

mv /var/lib/pocketbase/pb_data "/var/lib/pocketbase/pb_data_corrupted_$(date +%s)"
mv /tmp/pb_restore/unpacked /var/lib/pocketbase/pb_data

# Thiết lập đúng User/Group sở hữu tiến trình PocketBase
chown -R pocketbase:pocketbase /var/lib/pocketbase/pb_data
chmod 700 /var/lib/pocketbase/pb_data
chmod 600 /var/lib/pocketbase/pb_data/*.db

Bước 6: Khởi Động Lại Hệ Thống và Xác Nhận Trạng Thái

Khởi động lại systemd service và kiểm tra endpoint /api/health:

systemctl start pocketbase.service
curl -i http://127.0.0.1:8090/api/health

Phản hồi chuẩn:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{"code":200,"message":"API is healthy.","data":{}}

Quy trình backup và restore yêu cầu băng thông đĩa ngẫu nhiên (Random 4K I/O) cực lớn để vừa xử lý snapshot vừa đảm bảo zero downtime cho ứng dụng. Khi triển khai trên hạ tầng KVM Cloud của tropic.host, nền tảng lưu trữ Enterprise NVMe SSD (PCIe 4.0) với thông số đọc ngẫu nhiên 4K QD1 vượt trên 50.000 IOPS cho phép lệnh .backup hoàn thành tức thì mà không gây nghẽn hàng đợi ghi I/O.

Đồng thời, môi trường ảo hóa KVM thuần không chia sẻ tài nguyên với chỉ số trộm chu kỳ CPU tuyệt đối bằng 0 (%st = 0.0%) đảm bảo tiến trình nén đa luồng zstd chạy ở mức ưu tiên thấp (idle scheduling) không làm tăng độ trễ p99 của các API call trực tiếp từ client. Hệ thống mạng 1–10 Gbps unmetered hỗ trợ sẵn thuật toán kiểm soát tắc nghẽn TCP BBR tại tropic.host giúp việc đẩy các gói dữ liệu nén hàng chục Gigabyte ra remote S3 bucket đạt tốc độ tối đa của đường truyền mà không làm suy hao chất lượng mạng của dịch vụ chính.

Câu hỏi thường gặp (FAQ)

PocketBase có thể xử lý bao nhiêu người dùng trên VPS 1 vCPU, 1 GB RAM?

Nhờ viết bằng Go và dùng SQLite WAL mode trên ổ NVMe, PocketBase có thể xử lý dễ dàng 10.000+ kết nối đồng thời với lượng RAM tiêu thụ dưới 50 MB.

Dữ liệu SQLite trong PocketBase có an toàn khi VPS bị mất điện không?

Có, khi cấu hình ở chế độ WAL (Write-Ahead Logging), mọi giao dịch đều được ghi vào log tuần tự trước khi commit, đảm bảo tính toàn vẹn tuyệt đối.