Hướng dẫn tự host Actual Budget trên VPS bằng Docker
Triển khai Actual Budget trên VPS với Docker Compose. Bài viết giải thích cách cấu hình data volume, thiết lập HTTPS bắt buộc, import dữ liệu ngân hàng và quy trình backup.
Bạn đang xây dựng cái gì
Actual Budget là một ứng dụng quản lý ngân sách theo phương pháp phong bì (envelope budgeting) có thể tự host, và đây là lựa chọn phổ biến khi người dùng tìm kiếm giải pháp thay thế YNAB mà họ có thể tự vận hành. Server chỉ gồm một container, một data volume và một tên miền HTTPS. Mọi nhu cầu quản lý ngân sách thông thường đều chạy mượt mà trên VPS nhỏ nhất mà bạn có thể thuê, vì server chủ yếu đóng vai trò lưu trữ và đồng bộ file.
Bạn nên hiểu rõ kiến trúc này trước khi bắt đầu. Bản thân ngân sách là một database SQLite nằm bên trong trình duyệt và trong từng ứng dụng di động. Server mà bạn sắp cài đặt đóng vai trò là một sync endpoint: nó lưu trữ danh sách tài khoản, các file ngân sách và log thay đổi để giúp điện thoại và laptop đồng bộ dữ liệu với nhau. Đó là lý do tại sao ứng dụng vẫn hoạt động khi server ngoại tuyến, và cũng là lý do việc mất server không làm mất ngân sách của bạn, miễn là vẫn còn một client giữ bản sao dữ liệu.
Tại sao máy chủ cần HTTPS
Actual yêu cầu HTTPS, đây không phải là thủ tục hình thức. Trình duyệt chỉ cung cấp Web Crypto API, giao diện mà Actual sử dụng cho mã hóa đầu cuối, trong ngữ cảnh mà đặc tả gọi là ngữ cảnh bảo mật (secure context). Một ngữ cảnh bảo mật là https:// hoặc http://localhost. Nếu bạn tải ứng dụng từ http://203.0.113.10:5006 trên trình duyệt của máy khác, các tính năng này sẽ không xuất hiện vì trình duyệt không cấp quyền cho trang web. Các bản build di động chính thức cũng từ chối URL máy chủ http:// không bảo mật.
Vì vậy, có hai thiết lập khả thi. Cách thứ nhất là đặt một chứng chỉ thực trên một tên miền thực phía trước container, đây là cách hướng dẫn này thực hiện. Cách thứ hai là cấp cho máy chủ một chứng chỉ tự ký với ACTUAL_HTTPS_KEY và ACTUAL_HTTPS_CERT, như tài liệu dự án đã nêu, và chấp nhận cảnh báo của trình duyệt trên mọi thiết bị. Một chứng chỉ miễn phí từ Let's Encrypt chỉ mất năm phút để thiết lập, vì vậy hãy chọn phương án đầu tiên.
Cài đặt Actual Budget bằng Docker Compose
Hãy cài đặt Docker trước nếu VPS của bạn mới khởi tạo. Nếu cú pháp file Compose còn mới lạ với bạn, hướng dẫn các kiến thức cơ bản về Docker Compose cho VPS sẽ giải thích các trường được sử dụng dưới đây.
sudo install -d -m 755 /opt/actual
sudo install -d -m 700 /opt/actual/dataTạo file /opt/actual/docker-compose.yml:
services:
actual:
image: actualbudget/actual-server:latest
container_name: actual
restart: unless-stopped
ports:
- '127.0.0.1:5006:5006'
volumes:
- ./data:/dataCó ba chi tiết trong file đó cần lưu ý.
Image được sử dụng là actualbudget/actual-server:latest, do dự án phát hành lên Docker Hub và được mirror tại ghcr.io/actualbudget/actual. Có một tag latest-alpine dành cho các máy chủ có tài nguyên thấp.
Container ghi mọi dữ liệu vào đường dẫn /data. Bên trong đó, bạn sẽ thấy server-files, chứa account.sqlite lưu thông tin đăng nhập và token phiên làm việc, cùng với user-files, nơi lưu trữ chính các file ngân sách. Bạn phải mount đường dẫn này, nếu không docker compose pull sẽ làm mất dữ liệu ngân sách của bạn. Bạn có thể dùng ACTUAL_DATA_DIR để thay đổi vị trí lưu, nhưng giữ nguyên mặc định là ổn.
Cổng được publish chỉ trên 127.0.0.1. Nếu chỉ dùng 5006:5006, Docker sẽ publish trên mọi giao diện mạng và tự ghi đè các quy tắc của ufw, khiến ứng dụng bị lộ ra Internet ngay cả khi bạn đã thiết lập firewall chặn tất cả. Vấn đề này được giải thích tại tại sao các cổng Docker publish lại bỏ qua ufw. Việc bind vào loopback đảm bảo chỉ có reverse proxy trên cùng máy chủ mới có thể truy cập được ứng dụng.
Khởi chạy ứng dụng:
cd /opt/actual
docker compose up --detach
docker compose logs -f actualLog sẽ ổn định khi server báo đã lắng nghe trên cổng 5006. Hãy kiểm tra cục bộ trước khi cấu hình DNS:
curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5006/Mã 200 nghĩa là ứng dụng đang hoạt động. curl: (7) Failed to connect nghĩa là container không chạy, và docker compose ps sẽ cho thấy nó đã thoát. Nguyên nhân thường gặp là vấn đề quyền truy cập trên volume được mount, lỗi này sẽ hiển thị dưới dạng dòng EACCES trong log.
Đặt chứng chỉ và tên miền thực phía trước
Trỏ bản ghi A về VPS, budget.example.com, và đợi cho đến khi nó phân giải thành công. Sau đó cài đặt nginx và cấp chứng chỉ. Hướng dẫn Certbot trên Ubuntu 24.04 với nginx bao gồm đầy đủ quy trình cấp chứng chỉ và bộ đếm thời gian gia hạn.
Khối proxy:
server {
listen 443 ssl;
http2 on;
server_name budget.example.com;
ssl_certificate /etc/letsencrypt/live/budget.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/budget.example.com/privkey.pem;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:5006;
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;
}
}client_max_body_size là dòng mà mọi người thường quên. File ngân sách được tải lên toàn bộ trong một lần đồng bộ đầy đủ. Nginx mặc định giới hạn request body là 1 MB, vì vậy khi file vượt quá kích thước này, quá trình đồng bộ sẽ thất bại với lỗi 413 Request Entity Too Large trong access log của nginx, trong khi ứng dụng chỉ hiển thị lỗi đồng bộ chung chung. Bản thân server cũng có các giới hạn riêng: ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB mặc định là 20 và ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB mặc định là 50, vì vậy hãy đặt giới hạn của nginx cao hơn giá trị nào áp dụng cho bạn.
Reload và kiểm tra:
sudo nginx -t && sudo systemctl reload nginx
curl -fsS -o /dev/null -w '%{http_code}\n' https://budget.example.com/Lần chạy đầu tiên: mật khẩu và file ngân sách đầu tiên
Mở https://budget.example.com trên trình duyệt. Màn hình đầu tiên yêu cầu bạn đặt mật khẩu máy chủ. Mật khẩu duy nhất này bảo vệ toàn bộ máy chủ, vì vậy hãy tạo một mật khẩu ngẫu nhiên dài và lưu lại ở nơi bạn có thể tìm thấy, ví dụ như trong trình quản lý mật khẩu Vaultwarden tự host. Không có tài khoản người dùng nào cần tạo. Máy chủ của Actual được thiết kế chỉ dùng một mật khẩu, nên việc chia sẻ ngân sách đồng nghĩa với việc chia sẻ mật khẩu đó.
Sau đó, hãy tạo một file ngân sách. Actual sẽ hỏi bạn có muốn bật mã hóa đầu cuối (end-to-end encryption) hay không. Hãy chọn có, khi đó máy chủ chỉ lưu trữ dữ liệu đã mã hóa (ciphertext), đây là lựa chọn đúng đắn cho dữ liệu tài chính trên một máy chủ thuê ngoài. Cái giá phải trả là: mật khẩu mã hóa không bao giờ được gửi đến máy chủ, vì vậy nếu bạn làm mất nó, file dữ liệu sẽ mất vĩnh viễn và không có cách nào khôi phục. Hãy ghi lại mật khẩu trước khi nhấn tiếp tục qua màn hình đó.
Hãy thiết lập số dư bắt đầu dựa trên số dư hiện tại từ ngân hàng của bạn thay vì nhập dữ liệu lịch sử nhiều năm. Phương pháp ngân sách phong bì (envelope budgeting) hoạt động dựa trên số tiền bạn đang có, nên việc không có lịch sử giao dịch không ảnh hưởng gì đến bạn.
Nhập dữ liệu giao dịch
Đây là phần mà sự trung thực quan trọng hơn sự nhiệt tình, vì quy trình nhập liệu là lý do chính khiến người dùng từ bỏ việc tự host ứng dụng quản lý ngân sách.
Nhập thủ công là phương pháp cơ bản và luôn hiệu quả. Đối với phương pháp phong bì, đây có thể coi là mục đích chính, vì việc tự tay nhập khoản chi giúp bạn nhận thức rõ hơn về hành vi tiêu dùng của mình.
Nhập file xử lý khối lượng dữ liệu lớn. Actual đọc được các định dạng CSV, QIF, OFX và QFX, và mọi ngân hàng đều xuất dữ liệu ra ít nhất một trong các định dạng này. Bạn thực hiện nhập theo từng tài khoản từ màn hình tài khoản, ánh xạ các cột một lần, và Actual sẽ ghi nhớ bố cục đó cho tài khoản đó.
Đồng bộ ngân hàng tự động có sẵn, nhưng cần một dịch vụ bên thứ ba vì máy chủ không thể tự kết nối với ngân hàng. Actual hỗ trợ SimpleFIN Bridge cho các ngân hàng Bắc Mỹ, Enable Banking cho châu Âu, Akahu cho New Zealand và Pluggy.ai cho Brazil. GoCardless vẫn được hỗ trợ nhưng không chấp nhận tài khoản mới. Bạn tự đăng ký với nhà cung cấp, tạo thông tin xác thực và thêm chúng vào máy chủ. Tính đến tháng 7 năm 2026, SimpleFIN Bridge thu phí 15 USD mỗi năm cho tối đa 25 tổ chức, các dịch vụ khác có mức giá khác nhau.
Có hai giới hạn cần chấp nhận trước khi bạn dựa vào tính năng này. Thông tin xác thực API nằm trên máy chủ và không được bảo vệ bởi mã hóa đầu cuối (end-to-end encryption), vì máy chủ cần sử dụng chúng. Ngoài ra, Actual không tự động truy vấn: đồng bộ hóa là một nút bạn phải nhấn, không phải là một tác vụ chạy ngầm.
Sao lưu, vì tất cả chỉ là file
Mọi dữ liệu bạn cần đều nằm trong /opt/actual/data. Không cần bước export hay script dump database nào cả.
Bẫy duy nhất là SQLite. Việc copy account.sqlite trong khi server đang ghi dữ liệu có thể dẫn đến việc sao chép một transaction chưa hoàn tất, và bạn sẽ không biết điều đó cho đến khi cần restore. Hãy dừng container trong vài giây khi thực hiện copy:
cd /opt/actual
docker compose stop
restic -r sftp:backup@backup.example.com:/srv/restic backup /opt/actual/data
docker compose startHãy lên lịch cho việc này bằng phương pháp trong sao lưu restic trên VPS, nội dung này bao gồm cách thiết lập repository, chính sách lưu trữ và quy trình restore. Hãy thực hiện thử quy trình restore. Một bản sao lưu chưa từng được restore chỉ là một sự phỏng đoán.
Các bản sao lưu phía client của Actual là một tính năng riêng biệt và rất đáng lưu tâm. Trình duyệt lưu giữ các bản sao gần nhất của file ngân sách, có thể truy cập từ menu file, giúp xử lý các tình huống như "tôi lỡ tay xóa nhầm danh mục" mà không cần can thiệp vào server.
Cập nhật máy chủ
cd /opt/actual
docker compose pull
docker compose up --detachCompose tạo lại container từ image mới rồi gắn lại volume cũ, nên dữ liệu vẫn được giữ nguyên. Hãy cập nhật cả các client. Phiên bản server và app nên gần nhau. Client quá cũ so với server có thể từ chối đồng bộ và báo không khớp phiên bản. Hãy backup trước khi nâng qua major version, vì migration chạy ở lần khởi động đầu tiên và không có đường quay lại phiên bản cũ. Actual vẫn xử lý được tag latest dạng floating vì state của nó là một thư mục chứa các file. App sử dụng database thực thì không như vậy. Tự host Chatwoot trình bày cách dùng tag cố định và dump trước khi nâng cấp, như quy trình này yêu cầu.
Những lỗi thường gặp và cách nhận biết
Ứng dụng tải được nhưng quá trình đồng bộ không bao giờ hoàn tất. Hãy kiểm tra log truy cập của nginx để tìm 413. Điều này có nghĩa là client_max_body_size đang được đặt quá thấp. Ngược lại, 502 xuất hiện nghĩa là nginx đang chạy nhưng container thì không.
Các tùy chọn mã hóa bị thiếu hoặc ứng dụng di động từ chối URL. Trang web không nằm trong ngữ cảnh bảo mật. Thanh địa chỉ sẽ hiển thị http:// kèm theo địa chỉ IP hoặc hostname không phải là localhost. Hãy sửa chứng chỉ thay vì tìm cách né tránh lỗi này.
Thông báo file ngân sách không tương thích với phiên bản hiện tại. Phiên bản client và server đã bị lệch nhau. Hãy cập nhật cả hai lên cùng một bản release rồi tải lại trang.
Container khởi động lại liên tục. Hãy đọc docker compose logs actual. Lỗi quyền truy cập tại /data nghĩa là thư mục được mount không cho phép user của container ghi dữ liệu. Lỗi address-in-use nghĩa là đã có tiến trình khác chiếm cổng 5006 trên loopback.
Lần tải đầu tiên có cảm giác chậm. Toàn bộ file ngân sách sẽ được tải về trình duyệt khi bạn mở nó. Đây là một lần truyền dữ liệu lớn, sau đó các thao tác đọc sẽ diễn ra cục bộ. Đây không phải là vấn đề về cấu hình server, và việc thêm RAM sẽ không cải thiện được tốc độ này.
FAQ
Actual Budget có cần HTTPS để hoạt động không?
Có, trên thực tế là cần. Tính năng mã hóa đầu cuối (end-to-end encryption) của Actual sử dụng Web Crypto API của trình duyệt, và các trình duyệt chỉ cung cấp API này trong ngữ cảnh bảo mật, nghĩa là https:// hoặc http://localhost. Nếu chạy qua HTTP thông thường từ một máy khác, các tính năng này sẽ không khả dụng, và ứng dụng di động chính thức cũng từ chối URL máy chủ HTTP không bảo mật. Hãy sử dụng chứng chỉ Let's Encrypt trên một hostname thực, hoặc chứng chỉ tự ký với ACTUAL_HTTPS_KEY và ACTUAL_HTTPS_CERT nếu bạn chỉ dùng trình duyệt trên máy tính.
Actual có thể tự động nhập giao dịch ngân hàng của tôi không?
Chỉ thông qua dịch vụ bên thứ ba mà bạn tự đăng ký: SimpleFIN Bridge ở Bắc Mỹ, Enable Banking ở Châu Âu, Akahu ở New Zealand, hoặc Pluggy.ai ở Brazil. GoCardless được hỗ trợ nhưng hiện không nhận tài khoản mới. Các thông tin xác thực API này nằm trên máy chủ của bạn và không được bảo vệ bởi mã hóa đầu cuối. Việc đồng bộ cũng là thủ công, bạn phải nhấn nút và không có tiến trình nào tự động chạy ngầm. Việc nhập file CSV, QIF, OFX và QFX không cần bất kỳ bên thứ ba nào.
Chính xác thì tôi cần sao lưu những gì?
Thư mục dữ liệu đã mount, đó là /opt/actual/data trong hướng dẫn này. Nó chứa server-files/account.sqlite với thông tin đăng nhập và phiên làm việc, cùng user-files chứa các file ngân sách. Hãy dừng container trước khi sao chép, vì sao chép một cơ sở dữ liệu SQLite đang hoạt động có thể dẫn đến dữ liệu bị ghi thiếu. Không có thành phần nào khác trên máy chủ lưu trữ trạng thái.
Chuyện gì xảy ra nếu tôi quên mật khẩu mã hóa?
File đó không thể khôi phục được. Mật khẩu không bao giờ được gửi đến máy chủ, đó chính là mục đích của mã hóa đầu cuối, vì vậy không có cách nào để reset và không có hỗ trợ kỹ thuật cho việc này. Hãy lưu nó vào trình quản lý mật khẩu ngay khi tạo file, và giữ một bản sao ở nơi không phụ thuộc vào chính máy chủ này.
Actual Budget cần cấu hình máy chủ như thế nào?
Rất thấp. Container chỉ phục vụ các tài nguyên tĩnh và file, còn các tính toán ngân sách diễn ra ngay trên trình duyệt. Một vCPU chia sẻ với 1 GB RAM là đủ để chạy mượt mà, và thư mục dữ liệu cho ngân sách gia đình với lịch sử vài năm cũng chỉ tốn vài chục megabyte. Áp lực lên ổ cứng đến từ các bản sao lưu và các container khác của bạn, không phải từ Actual. Nếu bạn đang chọn cấu hình cho một máy chủ chạy kèm các dịch vụ nặng hơn, thường thì máy chủ ảnh sẽ là yếu tố quyết định cấu hình tối thiểu, vì vậy hãy kiểm tra dung lượng RAM mà PhotoPrism và Immich thực sự cần trước khi chọn gói dịch vụ. Logic tương tự áp dụng cho các hệ thống media: việc transcoding quyết định cấu hình, trong khi giao diện trình duyệt như Halcyon, thứ biến thư viện Jellyfin thành một cửa hàng băng đĩa thập niên 90 cũng tốn ít tài nguyên tương đương với Actual.