
CoastMath 저장소는 다음 급여부터 마지막 인출까지 매년의 재무 상황을 계산하는 은퇴·재무 계획 도구다. 연방·주·지역 세금, 계좌와 인출 순서, Social Security, 의료비, 몬테카를로 및 과거 시장 스트레스 테스트, 시나리오, 가져온 거래 내역을 바탕으로 한 실제 지출을 모델링한다.
저장소에는 계산 엔진, API, 웹 앱이 모두 들어 있으며, 모든 데이터를 SQLite 파일 하나에 저장하는 단일 컨테이너로 실행한다.
설치와 첫 실행
실행 방법은 Docker, Docker 없이 실행, 핫 리로드를 사용하는 개발 환경 세 가지다. Docker 방식은 Docker Compose가 필요하다. Docker 없이 실행하거나 개발하려면 Python 3.14와 Node 22가 필요하다.
Docker로 실행하려면 다음 명령을 사용한다.
git clone https://github.com/blank-slate-labs/coastmath.git
cd coastmath
docker compose up --build -d
http://localhost:8000을 연다. 8000번 포트가 이미 사용 중이면 COASTMATH_PORT=8001 docker compose up --build -d로 실행하거나 .env.example을 복사해 만든 .env에서 COASTMATH_PORT를 설정한다. 각 설정에는 기본값이 있다.
데이터베이스와 서버가 첫 실행 때 생성하는 비밀 값은 Docker 볼륨 coastmath_coastmath_data에 저장된다. 따라서 docker compose down 후 다시 up해도 계획은 유지된다. docker compose down -v를 실행하면 계획을 포함해 볼륨 전체가 삭제된다. docker compose logs -f는 서버 로그를 계속 출력한다.
서버는 기본적으로 이 컴퓨터에서만 접속할 수 있다. 다른 기기에서 사용하려면 먼저 다른 기기에서 접속하기를 확인한다.
Docker 없이 실행하려면 다음 명령을 사용한다.
git clone https://github.com/blank-slate-labs/coastmath.git
cd coastmath
cd frontend
npm ci --legacy-peer-deps
npx vite build # writes the web app to backend/static
cd ../backend
python3.14 -m venv venv
venv/bin/pip install -r requirements.txt
export DATABASE_URL=sqlite:///./coastmath.db SESSION_COOKIE_SECURE=false
venv/bin/uvicorn app.main:app --port 8000
http://localhost:8000을 연다. 데이터베이스와 생성된 비밀 값(coastmath.db.secrets.json)은 backend/에 만들어진다. 새 버전을 가져온 뒤에는 npx vite build를 다시 실행한다.
핫 리로드를 사용하는 개발 환경에서는 터미널 두 개를 연다. 앞의 절차를 마친 뒤 backend/에서 API를 실행한다.
export DATABASE_URL=sqlite:///./coastmath.db SESSION_COOKIE_SECURE=false
venv/bin/uvicorn app.main:app --port 8000 --reload
frontend/에서 웹 앱을 실행한다.
npm run dev -- --port 3000
http://localhost:3000을 연다. Vite는 /api 요청을 8000번 포트로 전달한다. API가 다른 포트에서 실행 중이면 VITE_API_PROXY_TARGET=http://localhost:<port>를 설정한다. 포트 3000을 사용하거나, 선택한 포트를 ALLOWED_ORIGINS에 추가해야 한다.
계정과 계획 입력
계정은 브라우저 세션으로 관리한다. 브라우저에서 처음 방문하면 해당 브라우저용 계정이 만들어지며 별도 로그인이 없다. 계획은 세션 쿠키에 연결되고 쿠키는 30일간 유지되며 방문할 때마다 갱신된다. 해당 사이트의 쿠키를 지우면 계획에 접근할 수 없고, 다른 브라우저를 사용하면 별도 계정이 만들어진다. localhost와 127.0.0.1도 서로 다른 사이트로 취급돼 각각 계정이 생긴다.
첫 방문 시 빈 계획이 열린다. 출생 연도와 주를 입력하고 계획을 만든다. 예상 계산을 실행하려면 사이드바의 필수 항목에 계좌, 수입원, 지출을 입력한다. ‘Success odds’ 탭에서는 몬테카를로 테스트를 실행한다.
CoastMath iPhone 앱은 자체 호스팅 서버가 아니라 호스팅 서비스에만 연결된다.
오류 해결
curl http://localhost:8000/api/ready가 {"status":"ready",…}를 반환하지 않으면 서버가 실행되지 않은 상태다. docker compose logs 또는 uvicorn 출력을 확인한다.
‘Address already in use’ 오류는 다른 프로그램이 8000번 포트를 사용 중이라는 뜻이다. Docker에서는 COASTMATH_PORT, uvicorn에서는 --port로 다른 포트를 지정한다.
새로고침 후 계획이 사라지면 브라우저에서 세션 쿠키가 삭제됐는지 확인한다. 일반 HTTP에서는 SESSION_COOKIE_SECURE를 false로 설정해야 브라우저가 세션 쿠키를 유지한다.
http://localhost:8000에서 {"error":{"code":"NOT_FOUND",…}}가 반환되면 웹 앱이 빌드되지 않은 것이다. frontend/에서 npx vite build를 실행하고 서버를 다시 시작한다.
HTTPS 프록시 뒤에서 저장할 때마다 403 ORIGIN_INVALID가 발생하면 서버가 프록시 헤더를 신뢰하지 않아 요청을 http://로 인식하고 https:// 출처를 거부하는 것이다. 프록시 주소를 FORWARDED_ALLOW_IPS에 설정한다. 자세한 내용은 다른 기기에서 접속하기를 참고한다.
python3.14: command not found가 나오면 Python 3.14를 설치한다. python.org 또는 패키지 관리자를 이용할 수 있으며, 이전 버전은 지원되지 않는다.
설정
환경 변수로 설정하며, 실행 방식에 따라 읽는 위치가 다르다. Docker Compose는 docker-compose.yml과 같은 디렉터리의 .env를 읽는다. .env.example에는 DATABASE_URL과 VITE_TRANSACTIONS_ENABLED를 제외한 모든 설정이 있으며, 두 설정은 docker-compose.yml에서 지정한다. uvicorn은 backend/.env와 셸 환경을 읽는다. COASTMATH_PORT와 COASTMATH_BIND는 Compose 설정이므로 uvicorn에서는 --port와 --host를 사용한다.
| 변수 | 기본값 | 설명 |
|---|---|---|
DATABASE_URL | 이미지에서는 sqlite:////data/coastmath.db | SQLite 파일 경로다. 시작 시 스키마를 만들거나 업그레이드한다. 파일은 서버 사용자만 읽을 수 있게 생성된다. |
SESSION_SECRET | 생성됨 | 세션 쿠키에 서명한다. 직접 설정하지 않으면 첫 실행 때 <database>.secrets.json에 생성된다. 32자 이상이어야 한다. |
LEDGER_ENCRYPTION_KEY | 생성됨 | 저장된 SimpleFIN 및 Plaid 인증 정보를 암호화한다. SESSION_SECRET과 마찬가지로 자동 생성된다. 키를 바꾼 뒤에도 이전 키로 암호화한 데이터를 읽으려면 LEDGER_ENCRYPTION_KEYS_PREVIOUS를 설정한다. |
SESSION_COOKIE_SECURE | Compose에서는 false | HTTPS로 제공할 때는 true로 설정한다. 일반 HTTP에서는 false여야 한다. 그렇지 않으면 브라우저가 세션 쿠키를 삭제한다. |
ALLOWED_ORIGINS | http://localhost:3000 | 쿠키를 포함해 API를 호출할 수 있는 다른 출처다. 서비스를 제공하는 출처는 항상 허용된다. *와 null은 시작 시 거부된다. |
FORWARDED_ALLOW_IPS | 127.0.0.1 | 서버가 보는 프록시 주소 가운데 X-Forwarded-* 헤더를 신뢰할 주소다. 역방향 프록시가 HTTPS를 종료하는 경우 설정한다. |
TRANSACTIONS_ENABLED | Compose에서는 true | 거래 내역 가져오기, 연결, 월별 비교를 제어한다. 끄더라도 거래 내역을 볼 수 있고 삭제할 수 있다. |
VITE_TRANSACTIONS_ENABLED | Compose에서는 true | 계획 도구에 거래 내역 섹션을 표시하는 빌드 인수다. |
WEB_CONCURRENCY | 1 | 서버 프로세스 수다. 한 가구에서는 하나면 충분하며 SQLite는 한 번에 한 프로세스만 쓸 수 있다. |
ENGINE_CONCURRENCY | 4 | 서버 프로세스 하나가 동시에 실행하는 예상 계산 수다. 이 수를 넘는 요청은 429로 거부된다. |
ENABLE_API_DOCS | 꺼짐 | 켜면 /openapi.json과 /docs에서 OpenAPI 스키마를 제공한다. |
ENABLE_STATELESS_API | 꺼짐 | 켜면 세션 없이 요청 본문에 담긴 계획을 계산하는 POST /api/simulate/stateless와 /api/compare/stateless를 제공한다. 로컬 도구와 테스트용이다. |
APP_REVISION | 설정되지 않음 | GET /api/ready의 revision 값으로 배포한 커밋 등을 표시한다. |
COASTMATH_PLUGINS | 설정되지 않음 | 시작 시 불러올 확장 모듈을 쉼표로 구분해 지정한다. 설정하지 않거나 비워 두면 불러오는 모듈이 없다. |
다른 기기에서 접속하기
Compose는 포트를 127.0.0.1에만 공개한다. 포트에 접근할 수 있는 사람은 누구나 계정을 만들 수 있다. 일반 HTTP에서는 세션 쿠키와 입력한 모든 정보가 암호화되지 않은 상태로 전송된다. 네트워크에 공개하기 전 다음을 설정한다.
- Caddy, nginx, Traefik 같은 HTTPS 종료 역방향 프록시를 앞에 두고
127.0.0.1:8000으로 전달한다. .env에서SESSION_COOKIE_SECURE=true로 설정한다.- 컨테이너가 보는 프록시 주소를
FORWARDED_ALLOW_IPS에 설정한다. Docker 호스트에서 실행하는 프록시는 기본적으로 Docker 브리지 게이트웨이(172.17.0.1)에서 들어온다. 다른 컨테이너의 프록시는 해당 네트워크 주소에서 들어오며,172.16.0.0/12는 Docker 기본 범위를 포함한다. 이 설정이 없으면 서버가 요청을http://로 인식하고https://출처를 다른 출처로 간주해 모든 저장 요청을 403ORIGIN_INVALID로 거부한다. - 프록시가 다른 기기에서 실행되는 경우에만
.env에서COASTMATH_BIND=0.0.0.0으로 설정해 모든 인터페이스에 포트를 공개하고, 방화벽에서 해당 프록시만 접근하도록 제한한다.
거래 내역 연동
가져온 거래 내역은 계획의 지출 계산과 월별 실제 지출 비교에 사용된다. 데이터 출처는 세 가지이며 각각 선택 사항이다.
- 파일: 은행에서 내려받은 CSV 및 OFX/QFX 내보내기 파일을 사용한다. 키가 필요 없다.
- SimpleFIN: Compose에서는
SIMPLEFIN_ENABLED가 켜져 있다. 사용자가 자신의 SimpleFIN Bridge 계정에서 받은 설정 토큰을 입력한다. 계좌는 12시간마다 동기화되며, 브리지는 연결 하나당 하루 약 24회 요청을 허용한다. 토큰이 가리킬 수 있는 브리지 호스트는SIMPLEFIN_ALLOWED_HOSTS에 지정한다. - Plaid:
PLAID_ENABLED를 켜고 사용자의 키를 입력한다. Plaid Dashboard에서 애플리케이션을 만들고PLAID_CLIENT_ID,PLAID_SECRET,PLAID_ENV를 설정한다.sandbox에서는 Plaid 테스트 은행을 바로 사용할 수 있다.production은 Plaid의 승인이 필요하며, Plaid는 연결된 로그인마다 키 보유자에게 요금을 청구한다. OAuth로 로그인하는 은행에는PLAID_REDIRECT_URI를 설정해야 한다. 값은https://<your host>/plaid/oauth이며 Plaid Dashboard에도 추가한다.PLAID_WEBHOOK_URL은https://<your host>/api/ledger/webhooks/plaid로 설정하면 Plaid에서 새 정보가 생길 때 동기화한다. 이 설정이 없으면 연결은 일정에 따라 동기화된다.PLAID_COUNTRY_CODES는 Link에서 제공할 국가를 지정하며 기본값은US다.
GET /api/ledger/providers는 제공되는 데이터 출처와 사용할 수 없는 이유를 알려준다. 인증 정보는 암호화해 저장하며 제공업체의 원본 데이터는 저장하거나 로그에 남기지 않는다. 거래 내역은 약 25개월간 보관한다. Plaid 연결을 끊거나 계정을 삭제하면 먼저 Plaid에서 Plaid 로그인을 삭제한다.
거래 내역 범주 추천
범주가 없는 거래에는 호스팅 또는 로컬의 OpenAI 호환 채팅 완성 엔드포인트를 이용해 범주를 추천할 수 있다.
LEDGER_LLM_ENABLED=true
LEDGER_LLM_BASE_URL=http://host.docker.internal:11434/v1 # e.g. a local Ollama
LEDGER_LLM_MODEL=<model name>
LEDGER_LLM_API_KEY= # if the endpoint needs one
host.docker.internal은 컨테이너에서 본 Docker 호스트 주소다. docker-compose.yml은 extra_hosts로 이 주소를 연결한다. Linux에서는 필요하지만 Docker Desktop에서는 무시된다.
추천은 사용자가 선택한 범주를 덮어쓰지 않는다. 거래 검토 과정에서 사용자가 추천을 수락해야 한다. 요청에는 설명, 가맹점, 입출금 방향, 정수 단위로 반올림한 금액, 제공업체의 범주 텍스트만 포함된다. 계좌 이름과 번호, 금융기관, 날짜는 포함되지 않는다. 호스팅 모델 제공업체는 자체 약관에 따라 이 텍스트를 받으며, 로컬 모델을 사용하면 데이터는 사용자 하드웨어를 벗어나지 않는다. LEDGER_LLM_TIMEOUT_SECONDS는 요청별 제한 시간이며 기본값은 30초다.
데이터와 백업
데이터 볼륨(coastmath_coastmath_data; docker volume ls로 확인 가능)에는 다음이 들어 있다.
- 실행 중에는
coastmath.db와coastmath.db-wal,coastmath.db-shm이 저장된다. - 생성된 키가 담긴
coastmath.db.secrets.json도 저장된다. 이 파일이 없으면 모든 세션에서 로그아웃되며 저장된 은행 연결을 다시 연결해야 한다.
암호화 키와 그 키로 암호화한 인증 정보는 같은 볼륨에 있다. 따라서 볼륨을 복사하면 저장된 모든 은행 연결의 내용을 읽을 수 있는 복사본이 된다. 백업도 서버만큼 엄격하게 보호해야 한다.
거래 내역의 ‘Download CSV’는 보관 기간 안에 있는 거래 내역을 내보내며, Exports 탭에서는 계획 결과를 내려받는다. 어느 쪽도 계획으로 다시 가져올 수 없으므로 계획을 백업하려면 볼륨을 백업해야 한다.
서버를 중지한 뒤 두 파일을 백업한다. 컨테이너에는 sqlite3 명령이 없고 볼륨은 호스트의 디렉터리가 아니다.
docker compose stop
docker run --rm -v coastmath_coastmath_data:/data -v "$PWD":/backup alpine \
tar czf /backup/coastmath_backup.tgz -C /data coastmath.db coastmath.db.secrets.json
docker compose start
또는 이미지에 포함된 Python과 SQLite 온라인 백업 기능을 이용해 실행 중인 데이터베이스를 복사할 수 있다.
docker compose exec coastmath python -c "import sqlite3; s = sqlite3.connect('/data/coastmath.db'); d = sqlite3.connect('/tmp/backup.db'); s.backup(d); d.close()"
docker compose cp coastmath:/tmp/backup.db ./coastmath_backup.db
docker compose cp coastmath:/data/coastmath.db.secrets.json ./coastmath_backup.db.secrets.json
복원할 때는 컨테이너를 중지하고 같은 방식으로 아카이브를 볼륨에 푼다. 이렇게 해야 파일 소유자가 서버 사용자로 유지된다. docker compose cp로 복사한 파일은 root 소유가 되어 서버가 쓸 수 없다.
tar xzf /backup/coastmath_backup.tgz -C /data
복원한 뒤 컨테이너를 시작한다. 업그레이드할 때는 새 버전을 가져온 뒤 docker compose up --build를 실행한다. 시작 과정에서 스키마가 마이그레이션된다. docker compose down은 볼륨을 유지하지만 docker compose down -v는 삭제한다.
개발과 라이선스
백엔드 테스트와 프런트엔드 검사는 다음 명령으로 실행한다.
cd backend
venv/bin/pip install -r requirements.txt -r requirements-dev.txt
venv/bin/python -m pytest tests/ -q
cd ../frontend
npm run lint
npm run typecheck
npm test
TEST_SQLITE_DIR=<dir>를 설정하면 백엔드 테스트에서 메모리 대신 데이터베이스 파일을 사용한다. 모델을 변경하면 SQLite 마이그레이션이 필요하다. 관련 명령은 backend/sqlite_alembic.ini에 있으며, 마이그레이션과 모델이 일치할 때까지 테스트가 실패한다.
계산 엔진 규칙과 수치는 backend/coastmath/에 있다. backend/coastmath/data/의 각 법정 수치에는 데이터 안에 출처가 기재돼 있다. 출처와 라이선스는 NOTICE.md에 정리돼 있다.
저작권은 Blank Slate Labs에 있으며 2026년 기준이다. 소스는 PolyForm Noncommercial License 1.0.0에 따라 공개된다. 라이선스가 정의하는 개인 사용, 자신과 가구를 위한 자체 호스팅, 연구, 수정, 비영리 단체의 사용은 허용된다. 별도 라이선스 없이 CoastMath를 판매하거나 유료·광고 지원 서비스로 제공하는 일, 사업에 사용하는 일 등 상업적 사용은 허용되지 않는다. 상업용 라이선스는 hi@blankslatelabs.dev로 문의할 수 있다. CoastMath 이름과 로고는 수정 버전이나 다른 제품에 사용할 수 없다.
현재 풀 리퀘스트는 받지 않으며 이슈는 환영한다. 자세한 내용은 CONTRIBUTING.md를 참고한다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요