SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-10-28

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.

Vì sao máy chủ cần HTTPS

Actual thực sự yêu cầu HTTPS, đây không phải thủ tục hình thức. Actual cần một tính năng của trình duyệt gọi là SharedArrayBuffer để có thể hoạt động. Trình duyệt chỉ bật tính năng này khi trang được cô lập cross-origin trong một secure context theo cách gọi của đặc tả. Secure context là https:// hoặc http://localhost. Nếu mở ứng dụng từ http://203.0.113.10:5006 bằng trình duyệt trên một máy khác, trình duyệt sẽ không cung cấp tính năng đó cho trang, nên budget không tải được. Điện thoại cũng không được miễn trừ. FAQ của Actual cho biết một ứng dụng iOS chính thức đang được beta testing và kết nối đến máy chủ của bạn. Actual cũng cho biết hiện chưa có ứng dụng Android chính thức. Vì vậy, trên hầu hết điện thoại, bạn dùng cùng web app đó và cài vào màn hình chính dưới dạng Progressive Web App. Quy tắc secure context tương tự vẫn áp dụng.

Có 2 cách thiết lập khả thi. Đặt certificate hợp lệ cho một tên miền thực phía trước container. Đây là cách hướng dẫn này sử dụng. Hoặc cấp cho máy chủ một self-signed certificate bằng ACTUAL_HTTPS_KEY và ACTUAL_HTTPS_CERT, theo tài liệu của project, rồi chấp nhận cảnh báo của trình duyệt trên mọi thiết bị. Certificate miễn phí từ Let's Encrypt chỉ mất 5 phút để cấp, vì vậy hãy chọn cách đầu tiên.

Cài đặt Actual Budget với 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ơ 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/data

Tạ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:/data

Có ba chi tiết trong file này cần lưu ý.

Image được sử dụng là actualbudget/actual-server:latest, do dự án phát hành trê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ấu hình 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 chứa các file ngân sách thực tế. 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 để di chuyển vị trí lưu trữ, nhưng giữ mặc định là ổn.

Cổng được publish chỉ trên 127.0.0.1. Nếu để trống như 5006:5006, Docker sẽ publish trên mọi interface mạng. Docker tự chèn các rule của nó vào trước ufw, vì vậy ứng dụng sẽ 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 được publish bởi Docker lại bypass ufw. Việc bind vào loopback đảm bảo rằng chỉ 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 actual

Log 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/

Kết quả 200 nghĩa là ứng dụng đang hoạt động. curl: (7) Failed to connect nghĩa là không có tiến trình nào chấp nhận kết nối tại 127.0.0.1:5006. Hãy chạy docker compose ps --all, vì lệnh docker compose ps thông thường chỉ liệt kê các container đang chạy. Nếu container hiển thị trạng thái exited, nguyên nhân thường là do vấn đề phân quyền trên volume được mount, bạn có thể thấy lỗi này qua dòng EACCES trong log.

Cấu hình chứng chỉ và tên miền thực tế

Trỏ một 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. Việc đồng bộ hàng ngày chỉ gửi các thay đổi của bạn, nhưng toàn bộ tệp ngân sách sẽ được tải lên khi bạn đăng ký ngân sách với máy chủ lần đầu tiên và mỗi khi bạn reset đồng bộ. Nginx mặc định giới hạn request body ở mức 1 MB, vì vậy khi tệp vượt quá kích thước này, các lần tải lên đầy đủ sẽ thất bại với mã 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. Máy chủ 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 của bạ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 thiết lập 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 nó ở nơi bạn có thể tìm lại, ví dụ như trong trình quản lý mật khẩu Vaultwarden tự host. Mặc định không có tài khoản người dùng nào để tạo: máy chủ chỉ có 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 đó. Việc đăng nhập riêng biệt chỉ khả thi nếu bạn kết nối với một nhà cung cấp OpenID, điều mà Actual hỗ trợ cho các thiết lập đa người dùng.

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ữ 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à thực tế: 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 này.

Hãy thiết lập số dư bắt đầu từ các con số hiện tại trong 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 lập ngân sách theo phong bì (envelope budgeting) hoạt động dựa trên số tiền bạn đang có hiện tại, vì vậy việc không có lịch sử giao dịch không ảnh hưởng gì đến bạn.

Nhập giao dịch

Đây là lúc tính trung thực quan trọng hơn sự hào hứng, vì khả năng import là lý do chính khiến nhiều người từ bỏ việc tự host ứng dụng lập ngân sách.

Nhập thủ công là cách cơ bản và luôn hoạt động. Với phương pháp phong bì, đây có thể xem là mục tiêu chính, vì việc tự nhập một khoản mua hàng khiến bạn chú ý đến khoản chi đó.

Import file xử lý phần lớn dữ liệu. Actual đọc được CSV, QIF, OFX, QFX và CAMT, và ngân hàng nào cũng hỗ trợ xuất ít nhất một trong các định dạng này. Import từng account từ màn hình account, rồi map các cột nếu file là CSV.

Có thể đồng bộ ngân hàng tự động, nhưng cần một dịch vụ bên thứ ba vì server 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 nhận account mới. Bạn tự đăng ký với nhà cung cấp, tạo thông tin xác thực rồi thêm chúng vào server. Tính đến October 2026, SimpleFIN Bridge tính phí 15 US dollars mỗi năm cộng thuế cho tối đa 25 tổ chức; các dịch vụ còn lại có mức giá khác.

Có 2 giới hạn bạn cần chấp nhận trước khi phụ thuộc vào tính năng này. Thông tin xác thực API nằm trên server và không được bảo vệ bằng mã hóa đầu cuối, vì server phải sử dụng chúng. Ngoài ra, không có dữ liệu nào được sync khi Actual đã đóng: automatic syncing mặc định bị tắt, và sau khi bật, tính năng này chỉ chạy khi app đang mở, nên server không tự polling ngân hàng.

Sao lưu vì chỉ có file

Mọi thứ bạn cần đều nằm trong /opt/actual/data. Không có bước export và cũng không có database dump cần viết script.

Điểm dễ mắc lỗi là SQLite. Nếu copy account.sqlite trong lúc server đang ghi dữ liệu, bạn có thể chụp phải một transaction chưa hoàn tất và chỉ phát hiện ra khi thử restore. Hãy stop container trong vài giây cần để copy:

cd /opt/actual
docker compose stop
restic -r sftp:backup@backup.example.com:/srv/restic backup /opt/actual/data
docker compose start

Đưa việc này vào schedule theo cách trình bày trong sao lưu restic trên VPS, trong đó có hướng dẫn setup repository, retention và thực hiện restore drill. Hãy thực hiện restore drill. Một bản backup chưa từng được restore chỉ là phỏng đoán.

Tính năng automatic backup riêng của Actual là một cơ chế khác và cũng đáng biết, nhưng chỉ có trong desktop app. Actual giữ tối đa 10 bản copy gần đây của file ngân sách, có thể truy cập từ File > Load Backup. Tính năng này xử lý được trường hợp “tôi lỡ xóa một category” mà không cần đụng đến server. Web app chạy trong browser không giữ các bản copy như vậy, nên nếu mọi người trong nhà đều dùng browser thì bản backup trên server là bản duy nhất bạn có.

Cập nhật máy chủ

cd /opt/actual
docker compose pull
docker compose up --detach

Compose tạo lại container từ image mới và gắn lại cùng volume, nên dữ liệu được bảo toàn. Hãy cập nhật cả các client. Client cũ hơn dữ liệu mà nó nhận được sẽ ngừng áp dụng thay đổi và hiển thị thông báo Update Required. Hãy backup trước khi nâng cấp phiên bản lớn, vì các migration sẽ chạy ngay lần khởi động đầu tiên và không có đường quay lại phiên bản cũ. Actual khá linh hoạt với tag latest dạng floating vì trạng thái của nó là một thư mục file, trong khi ứng dụng sử dụng database thực thụ thì không, và tự host Chatwoot sẽ hướng dẫn chi tiết về các tag cố định (pinned) và việc dump dữ liệu trước khi nâng cấp mà thói quen này yêu cầu.

Nguyên nhân lỗi và những gì bạn sẽ thấy

Ứng dụng tải được nhưng quá trình đồng bộ không bao giờ hoàn tất. Kiểm tra access log của nginx để tìm 413. Giá trị client_max_body_size được đặt quá thấp. 502 thường có nghĩa là nginx không nhận được phản hồi có thể sử dụng từ upstream. Nguyên nhân thường gặp nhất là container đã dừng hoặc không listening trên cổng 5006, vì vậy trước tiên hãy kiểm tra docker compose ps --all và log của container.

Ứng dụng không tải được trên thiết bị khác hoặc báo thiếu SharedArrayBuffer. Trang này không nằm trong secure context. Thanh địa chỉ sẽ hiển thị http:// cùng với địa chỉ IP hoặc hostname không phải localhost. Hãy sửa certificate thay vì tìm cách bỏ qua lỗi.

Xuất hiện thông báo budget file không tương thích với phiên bản này. File đã được mở bằng phiên bản Actual mới hơn client bạn đang sử dụng. Hãy cập nhật client lên ít nhất phiên bản đó, hoặc khôi phục một backup được tạo trước khi phiên bản mới hơn ghi vào file, rồi tải lại.

Container khởi động lại liên tục. Đọc docker compose logs actual. Lỗi permission trên /data có nghĩa là container user không có quyền ghi vào directory được mount. Lỗi address-in-use có nghĩa là một tiến trình khác đang giữ cổng 5006 trên loopback.

Lần tải đầu tiên có vẻ chậm. Toàn bộ budget file được tải xuống browser khi bạn mở file. Đây là một lần truyền dữ liệu lớn, sau đó là các thao tác đọc local. Đây không phải vấn đề về sizing của server, và thêm RAM sẽ không thay đổi tốc độ này.

FAQ

Actual có cần HTTPS để hoạt động không?

Có, trên thực tế là cần. Actual cần tính năng SharedArrayBuffer của trình duyệt để chạy, và trình duyệt chỉ bật tính năng này trong secure context, tức là https:// hoặc http://localhost. Nếu truy cập bằng HTTP thuần từ một máy khác, app sẽ không tải. Ứng dụng iOS chính thức đang trong giai đoạn beta và chưa có ứng dụng Android chính thức, nên hầu hết điện thoại đều dùng cùng web app và chịu cùng quy tắc này. (FAQ) Hãy dùng chứng chỉ Let's Encrypt trên hostname thật, hoặc chứng chỉ tự ký cùng với ACTUAL_HTTPS_KEY và ACTUAL_HTTPS_CERT nếu bạn chấp nhận cảnh báo chứng chỉ trên mọi thiết bị.

Actual có thể tự động import 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. API credential này nằm trên server của bạn và không được bảo vệ bằng mã hóa đầu cuối. Tự động đồng bộ bị tắt mặc định và sau khi bật chỉ chạy khi Actual đang mở, nên server không có tiến trình nào chạy nền để polling. Việc import CSV, QIF, OFX, QFX và CAMT hoàn toàn không cần bên thứ ba.

Chính xác thì tôi cần backup những gì?

Thư mục dữ liệu đã mount, tức /opt/actual/data trong hướng dẫn này. Thư mục này chứa server-files/account.sqlite với thông tin đăng nhập và session, cùng user-files với các file ngân sách. Hãy dừng container trước khi sao chép, vì sao chép một database SQLite đang hoạt động có thể ghi lại một lần ghi chưa hoàn tất. Không có thành phần nào khác trên server lưu state.

Điều gì xảy ra nếu tôi mất mật khẩu mã hóa?

File đó không thể khôi phục. Mật khẩu không bao giờ được gửi đến server, đây chính là mục đích của mã hóa đầu cuối, nên không có cách reset và cũng không có kênh hỗ trợ để khôi phục. Hãy lưu mật khẩu vào password manager ngay khi tạo file, đồng thời giữ một bản sao ở nơi không phụ thuộc vào chính server này.

Actual cần server mạnh đến mức nào?

Rất ít. Container phục vụ các static asset và file, còn việc tính toán ngân sách diễn ra trong trình duyệt. Một vCPU dùng chung với 1 GB RAM có thể chạy Actual ổn định, còn thư mục dữ liệu của một ngân sách gia đình có lịch sử vài năm thường chỉ chiếm vài chục megabyte. Disk pressure đến từ backup và các container khác, không phải từ Actual. Nếu bạn đang chọn cấu hình cho một máy phải chạy thêm ứng dụng nặng hơn, photo server thường là yếu tố quyết định mức tối thiểu, vì vậy hãy xem PhotoPrism và Immich thực sự cần bao nhiêu RAM trước khi chọn plan. Media stack cũng tuân theo logic này: transcoding quyết định plan, còn browser front end như Halcyon, biến thư viện Jellyfin thành một cửa hàng video 90s có thể đi dạo thường tốn tài nguyên ít như Actual.

Bài viết này có hữu ích không?