VPSにActual BudgetをDocker Composeで構築する方法
Docker ComposeでActual BudgetをVPSに構築します。データボリューム、ブラウザーにHTTPSが必要な理由、初回の予算ファイル、銀行データのインポート、バックアップ方法を解説します。
構築するもの
Actual Budget はセルフホスト型の封筒式予算管理アプリです。自分でホストできる YNAB の代替を探す場合、一般的な選択肢になります。サーバーは 1 つのコンテナ、1 つのデータボリューム、1 つの HTTPS 名で構成されます。通常の予算管理に必要な処理は、借りられる最小構成の VPS でも十分に動作します。サーバーが主にファイルの保存と同期を行うためです。
コマンドを入力する前に、アーキテクチャを理解しておく必要があります。予算データ自体は SQLite データベースです。このデータベースはブラウザー内と各モバイルアプリ内に保存されます。これからインストールするサーバーは同期エンドポイントです。アカウント一覧、予算ファイル、変更ログを保持し、スマートフォンとノートパソコンの状態を一致させます。そのため、サーバーが停止してもアプリは動作します。また、いずれかのクライアントにコピーが残っている限り、サーバーを失っても予算データは失われません。
サーバーに HTTPS が必要な理由
Actual は HTTPS を必要とします。これは形式的な要件ではありません。ブラウザーが Web Crypto API を公開するのは、仕様上のセキュアコンテキストに限られます。Actual はこの API をエンドツーエンド暗号化に使用します。セキュアコンテキストは https:// または http://localhost です。別のマシンのブラウザーで http://203.0.113.10:5006 からアプリを読み込むと、これらの機能は利用できません。ブラウザーがページに API を提供しないためです。公式モバイルビルドも、通常の http:// サーバー URL を拒否します。
実用的な構成は2つあります。コンテナの前段に、正式な名前で取得した実証済みの証明書を設定する方法です。このガイドではこの方法を使用します。もう1つは、プロジェクトのドキュメントに従って ACTUAL_HTTPS_KEY と ACTUAL_HTTPS_CERT で自己署名証明書をサーバーに設定し、すべてのデバイスでブラウザーの警告を受け入れる方法です。Let's Encrypt の無料証明書は5分で取得できるため、前者を選択してください。
Docker Compose で Actual Budget をインストールする
新しいサーバーであれば、最初に Docker をインストールします。Compose ファイルの構文に慣れていない場合は、VPS 向け Docker Compose の基本で以下のファイルに使用する項目を説明しています。
sudo install -d -m 755 /opt/actual
sudo install -d -m 700 /opt/actual/data/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このファイルでは、3 つの点が重要です。
イメージは actualbudget/actual-server:latest です。プロジェクトが Docker Hub に公開しており、ghcr.io/actualbudget/actual にもミラーされています。低消費電力のマシン向けに latest-alpine タグもあります。
コンテナは /data 以下にすべてのデータを書き込みます。その中の server-files には、ログイン情報とセッショントークンを含む account.sqlite が保存されます。user-files には予算ファイル自体が保存されます。このパスをマウントしないと、次の docker compose pull で予算が破棄されます。ACTUAL_DATA_DIR で移動できますが、デフォルトのままで問題ありません。
ポートは 127.0.0.1 のみで公開します。単独の 5006:5006 はすべてのインターフェースで公開します。また、Docker は ufw より先に独自のルールを書き込むため、すべて拒否するファイアウォール設定でもアプリケーションがインターネットに公開されます。この挙動については、Docker の公開ポートが ufw を迂回する理由で説明しています。loopback にバインドすると、同じサーバー上のリバースプロキシだけが接続できます。
起動します。
cd /opt/actual
docker compose up --detach
docker compose logs -f actualサーバーがポート 5006 で待ち受けていると報告すれば、ログは落ち着きます。DNS を変更する前に、ローカルで確認します。
curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5006/200 なら、アプリケーションは応答しています。curl: (7) Failed to connect ならコンテナは実行されておらず、docker compose ps で終了したことを確認できます。よくある原因は、マウントしたボリュームの権限問題です。ログには EACCES 行として表示されます。
証明書と正式な名前を前段に設定する
A レコードを VPS の budget.example.com に向け、名前解決を待ちます。次に nginx をインストールして、証明書を発行します。Ubuntu 24.04 で nginx を使う Certbot ガイドでは、証明書の発行と更新タイマーについて詳しく説明しています。
プロキシブロック:
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 は忘れられやすい設定です。完全同期では、予算ファイル全体がアップロードされます。Nginx のリクエストボディの既定値は 1 MB です。そのため、ファイルがこのサイズを超えると同期に失敗します。nginx のアクセスログには 413 Request Entity Too Large が記録されますが、アプリには一般的な同期エラーだけが表示されます。サーバー側には別の上限があります。ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB の既定値は 20、ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB の既定値は 50 です。そのため、nginx の上限には、該当する方の値より大きい値を設定します。
設定を再読み込みしてテストします。
sudo nginx -t && sudo systemctl reload nginx
curl -fsS -o /dev/null -w '%{http_code}\n' https://budget.example.com/最初の実行: パスワードと最初の予算ファイル
ブラウザーで https://budget.example.com を開きます。最初の画面でサーバーのパスワードを設定します。この 1 つのパスワードでサーバー全体を保護するため、長くランダムなものを生成し、自分でホストする Vaultwarden パスワードマネージャーなど、後で見つけられる場所に保管してください。作成するユーザーアカウントはありません。Actual のサーバーは設計上、1 つのパスワードで保護されます。そのため、予算を共有することは、そのパスワードを共有することを意味します。
次に、予算ファイルを作成します。Actual からエンドツーエンド暗号化を有効にするかどうかを尋ねられます。有効にしてください。サーバーには暗号文だけが保存されるため、レンタルしたマシンで財務データを扱う場合に適しています。ただし、代償があります。暗号化パスワードはサーバーに届かないため、紛失するとファイルを復元できず、リセット方法もありません。その画面を先に進める前に、パスワードを書き留めてください。
開始残高には、何年分もの履歴をインポートするのではなく、銀行に表示されている現在の残高を設定します。エンベロープ予算は現在手元にある資金を起点に進めるため、履歴が空でも問題ありません。
入金データの取り込み
ここでは、熱意よりも正直さが重要です。インポートの使い勝手が、セルフホスト型の家計簿から離脱する主な理由だからです。
手入力が基本であり、常に動作します。エンベロープ方式では、手入力こそが目的ともいえます。購入内容を入力することで、その支出を意識できるためです。
大量のデータはファイルから取り込めます。Actual は CSV、QIF、OFX、QFX を読み込めます。どの銀行も、少なくともいずれかの形式でエクスポートできます。アカウント画面からアカウント単位でインポートし、列の対応付けを一度設定すると、Actual はそのアカウント用のレイアウトを記憶します。
銀行との自動同期も利用できます。ただし、サーバーが単独で銀行と通信することはできないため、サードパーティーサービスが必要です。Actual は、北米の銀行向けに SimpleFIN Bridge、欧州向けに Enable Banking、ニュージーランド向けに Akahu、ブラジル向けに Pluggy.ai をサポートしています。GoCardless も引き続きサポートされていますが、新しいアカウントは受け付けていません。利用者自身でプロバイダーに登録し、認証情報を生成して、サーバーに追加します。SimpleFIN Bridge は、2026 年 7 月時点で最大 25 の金融機関を対象に年間 15 US dollars を請求します。他のサービスの料金体系は異なります。
この機能に依存する前に、2 つの制限を理解しておいてください。API の認証情報はサーバー上に保存され、エンドツーエンド暗号化の対象にはなりません。サーバーが認証情報を使用する必要があるためです。また、Actual はポーリングを行いません。同期はボタンを押して実行する操作であり、バックグラウンドジョブではありません。
バックアップはファイルだけなので簡単です
必要なデータはすべて /opt/actual/data 配下にあります。エクスポート手順はなく、スクリプトで実行するデータベースダンプもありません。
注意が必要なのは SQLite です。サーバーが書き込み中の account.sqlite をコピーすると、トランザクションが完了していない状態で取得されることがあります。そのことに気付くのは、復元を試すときです。コピーにかかる数秒間だけ、コンテナを停止します。
cd /opt/actual
docker compose stop
restic -r sftp:backup@backup.example.com:/srv/restic backup /opt/actual/data
docker compose startVPS での restic バックアップの手順でスケジュールを設定してください。この手順では、リポジトリのセットアップ、保持期間、復元訓練を扱います。復元訓練を実行してください。一度も復元していないバックアップは、推測にすぎません。
Actual 自身のクライアント側バックアップは別の機能なので、把握しておく価値があります。ブラウザーは予算ファイルの最近のコピーを保持しており、ファイルメニューから利用できます。これにより、サーバーに一切触れずに「誤ってカテゴリを削除した」という事態に対処できます。
サーバーの更新
cd /opt/actual
docker compose pull
docker compose up --detachCompose は新しいイメージからコンテナを再作成し、同じボリュームを再接続するため、データは保持されます。クライアントも更新してください。サーバーとアプリのバージョンは近い状態に保つ必要があり、サーバーより大幅に古いクライアントは、バージョン不一致のメッセージを表示して同期を拒否することがあります。メジャーバージョンを大きく変更する前にバックアップを取得してください。初回起動時にマイグレーションが実行され、ダウングレードの手段がないためです。Actual は状態をファイルのディレクトリとして保持するため、可変の latest タグでも問題が起きにくい一方、実際のデータベースを使用するアプリにはこの方法は適しません。Chatwoot のセルフホスティングでは、固定タグの使用方法と、更新前にダンプを取得する運用について説明しています。
何が問題になり、何を確認できるか
アプリは読み込めるが、同期がいつまでも完了しない。 nginx のアクセスログで 413 を確認します。これは client_max_body_size が低すぎることを示します。一方、502 であれば nginx は稼働しており、コンテナが起動していないことを意味します。
暗号化オプションが表示されない、またはモバイルアプリが URL を拒否する。 ページがセキュアコンテキストではありません。アドレスバーには、IP アドレスまたは localhost ではないホスト名を使用した http:// が表示されます。回避策を使うのではなく、証明書を修正します。
予算ファイルがこのバージョンと互換性がないというメッセージが表示される。 クライアントとサーバーのバージョンがずれています。両方を同じリリースに更新してから、再読み込みします。
コンテナがループして再起動する。 docker compose logs actual を確認します。/data で権限エラーが発生する場合、マウントしたディレクトリにコンテナのユーザーが書き込めません。アドレス使用中のエラーは、loopback 上の 5006 を別のプロセスがすでに使用していることを示します。
初回の読み込みが遅く感じられる。 ファイルを開くと、予算ファイル全体がブラウザーにダウンロードされます。最初に大きな転送が発生し、その後はローカルで読み取ります。これはサーバーのリソース不足の問題ではないため、RAM を増設しても変わりません。
FAQ
Actual Budget の動作に HTTPS は必要ですか?
はい。実際には必要です。Actual のエンドツーエンド暗号化はブラウザーの Web Crypto API を使用します。この API は、https:// または http://localhost に該当するセキュアコンテキストでのみ公開されます。別のマシンから平文の HTTP で接続すると、これらの機能は利用できません。また、公式モバイルアプリは平文の HTTP サーバー URL を拒否します。実際のホスト名には Let's Encrypt 証明書を使用してください。デスクトップブラウザーだけを使用する場合は、ACTUAL_HTTPS_KEY と ACTUAL_HTTPS_CERT を指定した自己署名証明書も使用できます。
Actual は銀行取引を自動的にインポートできますか?
自分で登録したサードパーティサービスを介する場合に限り可能です。北米では SimpleFIN Bridge、欧州では Enable Banking、ニュージーランドでは Akahu、ブラジルでは Pluggy.ai を利用できます。GoCardless もサポートされていますが、新規アカウントは受け付けていません。これらの API 認証情報はサーバー上に保存され、エンドツーエンド暗号化の対象外です。同期も手動で行うため、ボタンを押して実行します。バックグラウンドで自動的にポーリングすることはありません。CSV、QIF、OFX、QFX のインポートには、サードパーティサービスは一切必要ありません。
具体的に何をバックアップする必要がありますか?
このガイドでは /opt/actual/data にマウントしているデータディレクトリです。そこにはログイン情報とセッションを含む server-files/account.sqlite と、予算ファイルを含む user-files があります。コピーする前にコンテナを停止してください。稼働中の SQLite データベースをコピーすると、書き込みの途中の状態が保存される可能性があります。サーバー上で状態を保持するものはほかにありません。
暗号化パスワードを失うとどうなりますか?
ファイルを復元できなくなります。エンドツーエンド暗号化の目的上、パスワードがサーバーに送信されることはありません。そのため、リセット手段もサポートによる復旧手段もありません。ファイルを作成したらすぐにパスワードマネージャーへ保存し、同じサーバーに依存しない場所にも複製を保管してください。
Actual Budget にはどの程度のサーバーが必要ですか?
必要なリソースはごくわずかです。コンテナは静的アセットとファイルを提供し、予算の計算はブラウザーで実行されます。共有 vCPU 1 基と RAM 1 GB があれば問題なく動作します。数年分の履歴がある家庭用予算でも、データディレクトリのサイズは数十 MB に収まります。ディスクを圧迫するのはバックアップやほかのコンテナであり、Actual ではありません。ほかの負荷の大きいサービスも同じホストで実行する場合は、通常、必要なスペックの下限を決めるのは写真サーバーです。そのため、プランを選ぶ前に PhotoPrism と Immich に実際に必要な RAM を確認してください。メディアスタックにも同じ考え方が当てはまります。プランを決めるのはトランスコード処理であり、Jellyfin のライブラリを歩いて回れる 90 年代のビデオ店に変える Halcyon のようなブラウザフロントエンドは、Actual と同程度に少ないリソースで動作します。