Actual Budget 셀프 호스팅: Docker Compose 구축 가이드
Docker Compose를 사용하여 VPS에 Actual Budget을 설치하는 방법을 상세히 안내합니다. HTTPS 설정이 필수적인 이유와 데이터 동기화 원리, 그리고 안정적인 예산 관리를 위한 백업 및 데이터 볼륨 구성 전략을 확인하십시오.
구축할 내용
Actual Budget은 셀프 호스팅이 가능한 봉투 예산 관리 앱이며, 직접 호스팅할 수 있는 YNAB 대안을 찾는 사용자들에게 주로 추천되는 솔루션입니다. 서버는 컨테이너 1개, 데이터 볼륨 1개, HTTPS 도메인 1개로 구성됩니다. 서버는 주로 파일을 저장하고 동기화하는 역할을 수행하므로, 일반적인 예산 관리 기능은 가장 저렴한 VPS에서도 원활하게 작동합니다.
명령어를 입력하기 전에 아키텍처를 이해하는 것이 중요합니다. 예산 데이터 자체는 브라우저와 각 모바일 앱 내부에 저장되는 SQLite 데이터베이스입니다. 지금 설치하려는 서버는 동기화 엔드포인트 역할을 하며, 계정 목록, 예산 파일, 그리고 휴대폰과 노트북 간의 데이터 일치를 위한 변경 로그를 보관합니다. 이러한 구조 덕분에 서버가 다운되어도 앱은 계속 작동하며, 클라이언트 중 하나라도 복사본을 가지고 있다면 서버를 잃어버려도 예산 데이터는 손실되지 않습니다.
서버에 HTTPS가 필요한 이유
Actual은 HTTPS를 요구하며, 이는 단순한 형식이 아닙니다. Actual이 정상적으로 작동하려면 SharedArrayBuffer이라는 브라우저 기능이 필요한데, 브라우저는 페이지가 명세에서 정의하는 보안 컨텍스트(secure context) 내에서 교차 출처 격리(cross-origin isolated) 상태일 때만 이 기능을 활성화합니다. 보안 컨텍스트는 https://이거나 http://localhost인 경우를 의미합니다. 다른 기기의 브라우저에서 http://203.0.113.10:5006를 통해 앱을 불러오면 브라우저는 해당 기능을 페이지에 제공하지 않으며, 결과적으로 예산(budget) 데이터가 로드되지 않습니다. 스마트폰도 예외는 아닙니다. Actual의 FAQ에 따르면 공식 iOS 앱은 베타 테스트 중이며 사용자의 서버에 연결하는 방식입니다. 아직 공식 Android 앱은 없으므로, 대부분의 스마트폰에서는 동일한 웹 앱을 프로그레시브 웹 앱(PWA)으로 홈 화면에 추가하여 사용하게 되며, 이때도 동일한 보안 컨텍스트 규칙이 적용됩니다.
따라서 실행 가능한 구성은 두 가지입니다. 이 가이드에서 설명하는 것처럼 컨테이너 앞단에 실제 도메인과 유효한 인증서를 배치하는 방법이 있습니다. 아니면 프로젝트 문서에 설명된 대로 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이 파일에서 중요한 세부 사항은 세 가지입니다.
이미지는 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보다 우선하는 규칙을 작성하므로 모든 거부(deny-all) 방화벽 설정에도 불구하고 애플리케이션이 인터넷에 노출됩니다. 이러한 예기치 못한 상황에 대한 설명은 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가 출력되면 127.0.0.1:5006에서 연결을 수락하는 서비스가 없다는 의미입니다. docker compose ps --all을 실행하십시오. 단순히 docker compose ps만 입력하면 실행 중인 컨테이너만 표시되기 때문입니다. 컨테이너가 exited 상태로 표시된다면, 일반적으로 마운트된 볼륨의 권한 문제일 가능성이 높으며 로그에서 EACCES 줄을 통해 확인할 수 있습니다.
인증서 적용 및 도메인 연결
VPS에 A 레코드를 지정하고 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를 엽니다. 첫 화면에서 서버 비밀번호를 설정하라는 메시지가 나타납니다. 이 단일 비밀번호가 서버 전체를 보호하므로, 길고 무작위적인 비밀번호를 생성하여 자체 호스팅 Vaultwarden 비밀번호 관리자와 같이 안전한 곳에 보관하십시오. 기본적으로 별도의 사용자 계정을 생성할 필요는 없습니다. 서버는 하나의 비밀번호로 운영되므로 예산을 공유하려면 해당 비밀번호를 공유해야 합니다. 다중 사용자 환경을 위해 Actual이 지원하는 OpenID 공급자를 연결하는 경우에만 개별 로그인이 가능합니다.
그다음 예산 파일을 생성합니다. Actual은 종단간 암호화(end-to-end encryption) 활성화 여부를 묻습니다. 금융 데이터를 임대 서버에 저장하는 경우이므로 '예'를 선택하여 암호문만 저장되도록 하는 것이 올바른 방법입니다. 단, 암호화 비밀번호는 서버로 전송되지 않으므로 분실 시 파일을 복구할 방법이 없으며 재설정도 불가능하다는 점을 유의하십시오. 해당 화면을 넘어가기 전에 반드시 비밀번호를 기록해 두십시오.
수년 치의 내역을 가져오기보다는 은행의 현재 잔액을 기준으로 시작 잔액을 설정하십시오. 봉투 예산법(envelope budgeting)은 현재 보유한 자금을 바탕으로 미래를 계획하는 방식이므로, 과거 내역이 없어도 운영에 지장이 없습니다.
거래 내역 가져오기
이 단계에서는 열정보다 정직함이 중요합니다. 가져오기 과정에서 겪는 어려움이 사용자가 셀프 호스팅 가계부를 포기하는 주된 이유이기 때문입니다.
수동 입력은 가장 기본적인 방법이며 항상 작동합니다. 봉투 예산법을 사용한다면 직접 구매 내역을 입력하는 과정 자체가 지출을 인지하게 하므로 오히려 이 방식이 핵심일 수 있습니다.
파일 가져오기는 대량의 데이터를 처리할 때 사용합니다. Actual은 CSV, QIF, OFX, QFX, CAMT 형식을 읽을 수 있으며, 모든 은행은 최소한 이 중 하나 이상의 형식을 내보내기 지원합니다. 계정 화면에서 계정별로 가져오기를 수행하며, CSV 파일인 경우 열을 매핑해야 합니다.
자동 은행 동기화 기능도 존재하지만, 서버가 직접 은행과 통신할 수 없으므로 타사 서비스가 필요합니다. Actual은 북미 은행을 위한 SimpleFIN Bridge, 유럽을 위한 Enable Banking, 뉴질랜드의 Akahu, 브라질의 Pluggy.ai를 지원합니다. GoCardless도 여전히 지원되지만 신규 계정은 받지 않습니다. 사용자가 직접 해당 제공업체에 가입하여 자격 증명을 생성한 뒤 서버에 추가해야 합니다. 2026년 10월 기준으로 SimpleFIN Bridge는 최대 25개 기관에 대해 연간 15 US 달러(세금 별도)의 요금을 부과하며, 다른 서비스는 가격 정책이 다릅니다.
이 기능을 사용하기 전에 다음 두 가지 제한 사항을 인지해야 합니다. 첫째, API 자격 증명은 서버에 저장되며 서버가 이를 사용해야 하므로 종단 간 암호화(end-to-end encryption)가 적용되지 않습니다. 둘째, 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 start이 과정을 VPS에서의 restic 백업에서 설명하는 방식에 따라 일정에 맞춰 실행하십시오. 해당 문서에는 저장소 설정, 보관 주기, 복구 훈련 방법이 포함되어 있습니다. 반드시 복구 훈련을 수행하십시오. 한 번도 복구해 본 적 없는 백업은 추측에 불과합니다.
Actual 자체의 자동 백업 기능은 별개의 항목이며 알아둘 가치가 있지만, 이는 데스크톱 앱에서만 존재합니다. 데스크톱 앱은 예산 파일의 최근 복사본을 최대 10개까지 유지하며, File > Load Backup 메뉴에서 접근할 수 있습니다. 이는 서버를 건드리지 않고도 "실수로 카테고리를 삭제한 경우"를 해결할 수 있게 해줍니다. 브라우저에서 사용하는 웹 앱은 이러한 복사본을 유지하지 않으므로, 집안의 모든 사용자가 브라우저를 사용한다면 위에서 설명한 서버 백업이 유일한 복구 수단이 됩니다.
서버 업데이트
cd /opt/actual
docker compose pull
docker compose up --detachCompose는 새 이미지로 컨테이너를 다시 생성하고 동일한 볼륨을 다시 연결하므로 데이터는 유지됩니다. 클라이언트도 함께 업데이트하십시오. 데이터보다 버전이 낮은 클라이언트는 변경 사항 적용을 중단하고 Update Required 메시지를 표시합니다. 메인 버전이 크게 변경되는 경우 마이그레이션이 첫 시작 시점에 실행되며 다운그레이드 경로가 없으므로 반드시 사전에 백업을 수행하십시오. Actual은 상태가 파일 디렉터리 형태이므로 유동적인 latest 태그를 사용해도 문제가 없지만, 실제 데이터베이스를 사용하는 애플리케이션은 그렇지 않습니다. Chatwoot 셀프 호스팅 문서에서 권장하는 고정 태그 사용법과 업그레이드 전 덤프 절차를 확인하십시오.
오류 발생 시 증상 및 확인 사항
애플리케이션은 로드되지만 동기화가 완료되지 않습니다. nginx 접근 로그에서 413를 확인하십시오. 이는 client_max_body_size 설정값이 너무 낮기 때문입니다. 502이 발생한다면 nginx가 업스트림으로부터 유효한 응답을 받지 못한 것입니다. 대부분 컨테이너가 중지되었거나 5006 포트에서 대기 중이지 않은 경우이므로, 다른 작업을 수행하기 전에 docker compose ps --all와 컨테이너 로그를 먼저 확인하십시오.
다른 기기에서 애플리케이션이 로드되지 않거나 SharedArrayBuffer이 누락되었다는 메시지가 표시됩니다. 페이지가 보안 컨텍스트에서 실행되지 않고 있습니다. 주소창에 IP 주소나 localhost가 아닌 호스트 이름이 포함된 http://가 표시될 것입니다. 우회 방법을 찾기보다 인증서 문제를 해결하십시오.
예산 파일이 현재 버전과 호환되지 않는다는 메시지가 표시됩니다. 해당 파일이 현재 사용 중인 클라이언트보다 최신 버전의 Actual에서 열렸습니다. 클라이언트를 최소 해당 버전 이상으로 업데이트하거나, 최신 버전이 파일을 수정하기 전의 백업을 복원한 뒤 다시 로드하십시오.
컨테이너가 반복적으로 재시작됩니다. docker compose logs actual을 확인하십시오. /data에서 권한 오류가 발생한다면 마운트된 디렉터리에 컨테이너 사용자가 쓰기 권한을 가지고 있지 않은 것입니다. 주소 사용 중(address-in-use) 오류는 다른 프로세스가 이미 루프백에서 5006 포트를 점유하고 있음을 의미합니다.
첫 로딩이 느리게 느껴집니다. 예산을 열 때 전체 예산 파일이 브라우저로 다운로드됩니다. 이는 한 번의 대용량 전송 후 로컬에서 읽는 방식입니다. 서버 사양 문제가 아니므로 RAM을 추가해도 해결되지 않습니다.
FAQ
Actual Budget은 동작을 위해 HTTPS가 반드시 필요한가요?
실제 환경에서는 그렇습니다. Actual은 브라우저의 SharedArrayBuffer 기능을 사용해야 실행되는데, 브라우저는 보안 컨텍스트인 https:// 또는 http://localhost에서만 이 기능을 활성화합니다. 다른 기기에서 일반 HTTP로 접속하면 앱이 로드되지 않습니다. 공식 iOS 앱은 베타 테스트 중이며 공식 Android 앱은 아직 없으므로, 대부분의 휴대폰은 동일한 웹 앱을 사용하며 같은 규칙이 적용됩니다. (FAQ) 실제 호스트 이름으로 Let's Encrypt 인증서를 사용하거나, 모든 기기에서 인증서 경고를 감수할 수 있다면 ACTUAL_HTTPS_KEY 및 ACTUAL_HTTPS_CERT를 사용한 자체 서명 인증서를 사용하십시오.
Actual에서 은행 거래 내역을 자동으로 가져올 수 있나요?
사용자가 직접 가입하는 타사 서비스를 통해서만 가능합니다. 북미 지역은 SimpleFIN Bridge, 유럽은 Enable Banking, 뉴질랜드는 Akahu, 브라질은 Pluggy.ai를 이용할 수 있습니다. GoCardless도 지원하지만 현재 신규 계정은 받지 않습니다. 해당 API 자격 증명은 서버에 저장되며 종단간 암호화(end-to-end encryption) 대상이 아닙니다. 자동 동기화는 기본적으로 꺼져 있으며, 활성화하더라도 Actual이 열려 있는 동안에만 실행되므로 서버에서 백그라운드로 데이터를 조회하지 않습니다. CSV, QIF, OFX, QFX, CAMT 형식의 가져오기 기능은 타사 서비스가 전혀 필요하지 않습니다.
정확히 무엇을 백업해야 하나요?
이 가이드에서 /opt/actual/data으로 지정한 마운트된 데이터 디렉터리입니다. 여기에는 로그인 및 세션 정보가 담긴 server-files/account.sqlite와 예산 파일이 담긴 user-files가 포함되어 있습니다. 실행 중인 SQLite 데이터베이스를 복사하면 쓰기 작업이 일부 누락될 수 있으므로, 복사하기 전에 컨테이너를 중지하십시오. 서버의 다른 곳에는 상태 정보가 저장되지 않습니다.
암호화 비밀번호를 분실하면 어떻게 되나요?
파일을 복구할 수 없습니다. 비밀번호는 서버로 전송되지 않으며, 이것이 바로 종단간 암호화의 핵심이므로 비밀번호 재설정이나 지원 경로가 존재하지 않습니다. 파일을 생성하는 즉시 비밀번호 관리자에 저장하고, 이 서버에 의존하지 않는 별도의 장소에 사본을 보관하십시오.
Actual Budget을 운영하려면 어느 정도의 서버 사양이 필요한가요?
매우 낮은 사양으로도 충분합니다. 컨테이너는 정적 에셋과 파일을 제공하며, 예산 계산은 브라우저에서 수행됩니다. 1개의 공유 vCPU와 1 GB의 RAM만으로도 문제없이 실행되며, 수년간의 기록이 담긴 가계 예산 데이터 디렉터리도 수십 MB 수준에 불과합니다. 디스크 부하는 Actual이 아닌 백업이나 다른 컨테이너에서 발생합니다. 더 무거운 서비스를 함께 운영할 서버를 구성 중이라면, 보통 사진 서버가 최소 사양을 결정하므로 플랜을 선택하기 전에 PhotoPrism과 Immich가 실제로 필요로 하는 RAM 용량을 먼저 확인하십시오. 미디어 스택도 같은 논리가 적용됩니다. 트랜스코딩 작업이 플랜을 결정하며, Jellyfin 라이브러리를 90년대 비디오 대여점처럼 탐색하게 해주는 Halcyon과 같은 브라우저 프론트엔드는 Actual만큼이나 적은 자원을 소모합니다.