5편까지 룰을 깎았으면 다음은 어디서 돌릴 것인가 다. 처음에는 안 쓰는 Windows 노트북에 cron 을 박아 24/7 돌리면 된다고 생각했다. 결론적으로 일주일 만에 접고 Vultr Seoul VPS 로 갈아탔다. 1시간 안에 끝난 작업이지만 셋업의 거의 모든 단계에서 한 번씩 막혔고, 그게 더 재밌어서 회고로 남긴다.
왜 클라우드였나
노트북 24/7 cron 의 세 가지 한계가 차례로 드러났다.
- 노트북 sleep · 절전 모드 — 평일 15:20 cron 이 절전 중이면 그냥 안 돈다. wake on schedule 같은 옵션이 있긴 하지만 뚜껑이 닫히면 끝이다.
- DB 가 두 곳에 분산 — Mac 에서 백테스트하고 룰을 만든 뒤 운영 머신으로 옮길 때마다 export/import 가 필요했다.
- 두 머신에서 KIS API 동시 호출 위험 — 계좌가 1개인데 호스트가 2개면 사고 시나리오만 늘어난다.
결론은 VPS 가 single source of truth 가 되어야 cron · UI · 전략 관리가 하나의 흐름으로 정리된다는 것. Mac 은 그 VPS 에 SSH 터널로 붙어 쓰기·디버깅하는 단말 역할로 내려가면 된다.
호스팅 옵션
| 옵션 | 가격 | 위치 | 선택 사유 |
|---|---|---|---|
| AWS Lightsail | $10~ | Tokyo | 친숙하지만 1-2개월 테스트엔 과함 |
| Vultr | $10 | Seoul | 한국 IP (KIS 친화) + 가성비 |
| DigitalOcean | $12 | Singapore | UI 깔끔, Tokyo 없음 |
| Railway / Render | 가변 | - | docker-compose 통째 ❌ |
KIS 실전 전환 시에도 한국 IP 가 그대로 쓰일 수 있는 게 결정적이었다. Vultr Seoul 1vCPU / 2GB / 55GB SSD / $10/mo 로 확정.
셋업 단계와 실제 소요 시간
| 단계 | 소요 |
|---|---|
| Vultr 가입 + 인스턴스 생성 | 5분 |
| SSH + apt upgrade + Docker 설치 | 10분 |
Repo clone + .env + docker compose up -d | 3분 |
| Mac → VPS DB pg_dump/restore | 20분 |
| KIS dry-run 검증 | 1분 |
| cron 등록 + 시간대 KST 변경 | 1분 |
| 합계 | 약 1시간 |
문서를 따라가면 1시간이지만, 막히는 곳은 거의 다 paste 와 protocol 의 미묘한 차이 였다.
핵심 시행착오 3개
(a) scp 의 새 SFTP 백엔드와 ~/ 확장
OpenSSH 9.x 부터 scp 가 SFTP 를 기본 백엔드로 쓰는데, 일부 환경에서 source path 의 ~/ 확장이 깨진다.
# ❌ "/root/quant.dump: No such file or directory"
# Mac 의 ~ 가 안 풀림
scp ~/quant.dump root@vps:/root/
# ✅ -O (옛 SCP protocol) + 절대 경로
scp -O /Users/dongho/quant.dump root@vps:/root/
서브 함정: VPS 안 쉘에서 scp 를 쳐서 한 번 더 실수했다. 프롬프트가 root@quant-server:~# 인 상태에서 Mac 경로를 지정하면 당연히 못 찾는다. 출발지 (Mac) 에서 실행해야 한다는, 너무 당연한데 자주 헷갈리는 포인트.
(b) TimescaleDB dump · restore 정석
지난번 Mac (ARM) → Windows (x86) 시도가 실패했던 원인은 두 가지였다 — 아키텍처 차이 + TimescaleDB 의 pre_restore / post_restore 함수 누락. 정석은 다음 4단계.
# 1. extension 먼저
psql -d quant -c "CREATE EXTENSION timescaledb;"
# 2. pre_restore — hypertable[^hypertable] 메타 일시 비활성
psql -d quant -c "SELECT timescaledb_pre_restore();"
# 3. restore
pg_restore -d quant --no-owner --no-acl /tmp/quant.dump
# 4. post_restore — hypertable 메타 복구
psql -d quant -c "SELECT timescaledb_post_restore();"
dump 단계에서 NOTICE: hypertable data are in chunks, no data will be copied 가 떠서 잠깐 당황했지만, chunks 는 별도 테이블처럼 dump 떠지므로 무시해도 된다. 1GB dump 안에 다 들어있다.
이번엔 클라우드 → 클라우드 (둘 다 x86 Linux) 라 cross-arch 함정이 없었다. 같은 아키텍처면 TimescaleDB dump/restore 는 정석대로 깔끔하게 돈다.
(c) docker-compose 노출 포트는 기본적으로 외부 공개
docker compose up -d 직후 engine 로그에 처음 보는 외부 IP 가 /mcp, /jsonrpc, /security.txt 같은 경로를 GET/POST 하는 게 찍혔다. 인터넷 봇이 노출된 8000 / 8080 포트를 자동 스캔하는 거다. 30초도 안 걸렸다.
원인은 docker-compose.yml 의 ports: ["8000:8000"] 표기가 0.0.0.0:80001 에 바인딩된다는 것. 인터넷 누구나 접근 가능한 상태다. SSH 터널로만 붙으면 되니 외부에 열어둘 이유가 없다.
# Before — 0.0.0.0 바인딩 (외부 공개)
ports:
- "8000:8000"
- "8080:8080"
# After — localhost 만 허용
ports:
- "127.0.0.1:8000:8000"
- "127.0.0.1:8080:8080"
docker compose ps 의 PORTS 컬럼이 127.0.0.1:8000->8000/tcp 로 보이면 외부 차단 확인.
최종 운영 흐름
[Mac 브라우저] → localhost:3000
↓
[Mac 의 web (pnpm dev)]
↓ API 호출 → localhost:8080
[SSH 터널 (Mac:8080 → VPS:8080)]
↓
[VPS Go API] ←→ [VPS DB] ←→ [VPS engine cron]
| Cron (KST 평일) | 동작 |
|---|---|
| 08:00 | incremental ingest2 — 전 영업일 종가 1일치 |
| 15:20 | 라이브 매매 (paper, 동시호가 시장가 → 15:30 batch fill) |
미묘한 포인트 — 월요일 ingest 의 의미
처음에는 월요일 08:00 ingest 는 토 · 일 비거래일이라 noop 아니냐 고 생각했지만 틀렸다.
- 금요일 08:00 ingest → 목요일 종가까지만 수집
- 토 · 일은 cron 이 안 돈다 → DB 의 마지막 데이터는 목요일 종가
- 월요일 08:00 ingest 가 incremental 로 금요일 종가를 채운다
cron 을 화-금만 돌리면 안 되는 이유다. 시점 · 타임존의 이런 미묘함이 자가호스팅 자동매매의 디테일이다.
회고
VPS 셋업은 매뉴얼이 무서워 보이지만 실제로는 한 시간 안에 끝났다. 막히는 곳은 거의 다 paste 와 protocol 의 미묘한 차이 —
scp -O,.env의 trailing 공백,docker-composevsdocker compose. 시스템 자체의 복잡성보다 도구의 작은 변경 이력이 발목을 잡는다.
세 가지로 정리하면.
- 클라우드 → 클라우드 dump/restore 는 깔끔하다 — 같은 아키텍처면 TimescaleDB 도 정석대로 돈다. ARM ↔ x86 같은 cross-arch 마이그레이션은 권장하지 않는다.
- docker compose 노출 포트는 기본적으로 외부 공개 — 봇이 30초 안에 스캔을 시작한다.
127.0.0.1바인딩 + SSH 터널이 가장 단순하고 안전. .env의 trailing 공백 · CR 은 의외로 흔한 함정 —nano로 paste 하지 말고cat <<'EOF'heredoc3 으로 쓰는 편이 안전하다.
다음 편은 로컬 작업 → VPS 자동 반영 파이프라인 — Mac 에서 코드를 수정해 push 하면 VPS 의 컨테이너가 자동으로 갱신되도록 묶는 작업이 될 예정이다.
Footnotes
-
0.0.0.0 vs 127.0.0.1 바인딩 —
0.0.0.0은 모든 네트워크 인터페이스에서 listen, 즉 외부 인터넷에서도 접근 가능.127.0.0.1(loopback) 은 같은 호스트 안에서만 접근 가능. SSH 터널이 있다면 후자가 거의 항상 안전한 선택. ↩ -
incremental ingest — 전체 데이터를 새로 받지 않고, DB 의 마지막 적재 시점 이후 변경분만 추가하는 적재 방식. 한 번에 1일치만 받아 cron 부하·API 호출 한도가 작아진다. ↩
-
heredoc — shell 의
<<EOF ... EOF문법. 여러 줄 텍스트를 그대로 명령의 표준 입력으로 전달한다.'EOF'처럼 따옴표로 감싸면 변수 치환·이스케이프가 모두 비활성화돼서 paste 안전성이 높다. ↩