김솔비 블로그
기술 블로그

개인 프로젝트 인프라 설계: 도메인부터 집 서버, CI/CD까지 어떻게 왜 나눴나

10분 읽기

TL;DR

  • 원칙은 셋이다. 고정비 0원에 가깝게, 공개는 꼭 필요한 만큼만, 배포는 자동으로 하고 실패하면 스스로 되돌리기.
  • 방문자가 몰릴 수 있는 웹앱은 서버리스 호스팅 + Supabase에, 오래 돌거나 파일을 많이 다루는 일은 집의 라즈베리파이에 둔다.
  • 집 서버로 들어오는 길은 둘로 나눴다. 방문자는 Cloudflare Tunnel로, 나와 CI는 Tailscale로. 공유기 포트는 하나도 열지 않는다.

들어가며

회사 일과 별개로 개인 프로젝트를 여러 개 운영한다. 공개 웹 서비스도 있고, 나만 쓰는 자동화 도구도 있고, 주문이 들어오면 바로 반응해야 하는 시스템도 있다.

처음에는 프로젝트마다 그때그때 편한 곳에 올렸다. 그러다 보니 "이건 어디에 떠 있더라", "이 도메인은 어디서 관리하지", "배포는 어떻게 했더라"를 매번 다시 찾게 됐다. 그래서 한 번 전체를 정리하고, 어떤 종류의 일을 어디에 둘지 기준을 세웠다.

이 글은 그 기준과 이유를 정리한 것이다. 각 도구가 무엇인지는 따로 정리해 두었다.

원칙

원칙뜻
고정비 0원에 가깝게매달 나가는 돈은 도메인 값 정도. 트래픽이 없을 때 돈이 나가지 않는 구조
공개는 꼭 필요한 만큼만방문자가 볼 것만 인터넷에 열고, 관리 화면·SSH·내부 API는 사설망 안에만
배포는 자동, 실패는 스스로 복구푸시하면 테스트 → 배포 → 상태 확인. 실패하면 이전 버전으로 되돌린다
비밀값은 코드 밖에키와 토큰은 각 플랫폼의 환경변수와 서버의 권한 잠긴 파일에만

전체 그림

                         ┌──────────── 도메인 ────────────┐
                         │  등록: 가비아 · Cloudflare       │
                         │  DNS : Cloudflare 한 곳에 모음   │
                         └───────┬───────────────┬───────┘
                                 │               │
             ┌───────────────────▼──┐     ┌──────▼──────────────────────────┐
  방문자 ──▶ │ 서버리스 호스팅       │     │ Cloudflare (프록시 · Tunnel)     │
             │ (웹앱 · 블로그)       │     └──────┬──────────────────────────┘
             └─────────┬────────────┘            │ 바깥쪽 연결만 (포트 개방 없음)
                       │                         ▼
             ┌─────────▼────────────┐     ┌─────────────────────────────────┐
             │ Supabase             │     │ 라즈베리파이 (집)                 │
             │ DB · 인증 · 파일 · 예약 │     │ 오래 도는 서버 · 파일 · 자동화     │
             └──────────────────────┘     └──────▲──────────────────────────┘
                                                 │ Tailscale (사설망)
                                     ┌───────────┴───────────┐
                                     │ 내 Mac · 휴대폰         │
                                     │ GitHub Actions (임시)   │
                                     └───────────────────────┘

1. 도메인과 DNS: 사는 곳은 둘, 관리하는 곳은 하나

도메인은 가비아와 Cloudflare 두 곳에서 샀다.

  • .kr처럼 국내 도메인은 가비아에서 산다. Cloudflare Registrar가 지원하지 않는 확장자가 있다.
  • .com 같은 도메인은 Cloudflare Registrar가 원가로 팔아서 갱신 비용이 오르지 않는다.

대신 DNS는 모두 Cloudflare로 모았다. 가비아에서 산 도메인도 네임서버를 Cloudflare로 바꿨다.

왜: 프로젝트가 늘수록 서브도메인이 늘어난다. 레코드가 여러 업체 화면에 흩어져 있으면 무엇이 어디를 가리키는지 놓친다. 한 곳에 모으면 같은 화면, 같은 규칙으로 관리할 수 있고, 프록시·Tunnel·방화벽 같은 Cloudflare 기능도 바로 붙일 수 있다.

레코드는 대상에 따라 다르게 둔다.

대상레코드프록시
서버리스 호스팅에 올린 웹앱CNAME → 호스팅 주소끔(회색). 호스팅이 자체 CDN과 인증서를 가지고 있어서 겹치지 않게
집 서버의 공개 서비스Tunnel이 자동으로 만든 레코드켬(주황). 집 IP를 숨기고 HTTPS와 공격 차단을 Cloudflare가 맡는다

2. 어떤 일을 어디에 둘까

가장 중요한 결정이다. 기준은 "그 일이 어떤 모양인가"였다.

일의 모양둔 곳이유
방문자가 많을 수도 적을 수도 있는 공개 웹앱서버리스 호스팅 + Supabase트래픽이 없으면 비용 0, 몰려도 알아서 늘어난다. 서버 관리가 없다
오래 걸리는 작업, 큰 파일, 브라우저 자동화, 계속 켜져 있어야 하는 서버라즈베리파이서버리스는 실행 시간과 저장 공간에 한도가 있다. 집 서버는 고정비 0원이고 파일이 내 손을 떠나지 않는다
나만 쓰는 관리 도구라즈베리파이 + Tailscale인터넷에 열 필요가 없다. 사설망 안에서만 보이면 로그인 화면이 공격받을 일도 없다

서버리스 호스팅 + Supabase를 고른 이유

공개 웹앱은 Next.js나 React로 만들고 서버리스 호스팅에 올린다. 데이터는 Supabase에 둔다.

  • API 서버를 따로 안 만든다. 브라우저와 서버 컴포넌트가 Supabase를 바로 부르고, 보안은 DB 권한 규칙(RLS)과 칸 단위 권한이 맡는다.
  • 복잡한 규칙은 DB 함수로. "하루 한 번만 세기"처럼 검사가 필요한 쓰기는 표를 직접 열지 않고 함수 하나만 열어 둔다.
  • 예약 작업도 안에서. 매일 정리할 데이터는 DB 예약 작업으로, 매일 수집이나 주간 알림은 호스팅의 Cron으로 부른다.

대가: 무료 플랜의 한도(캐시 쓰기, 이미지 변환 횟수, DB 용량)를 늘 의식해야 한다. 그래서 처음부터 방문자 수와 DB 조회 수가 비례하지 않는 캐시 구조로 만들고, 쌓이는 기록은 보관 기간을 정해 지운다.

라즈베리파이를 고른 이유

  • 고정비 0원. 클라우드 VM은 작은 것도 매달 돈이 나간다. 이미 있는 기기와 전기료 정도면 된다.
  • 파일이 내 손 안에. 원본 파일이나 작업용 이미지를 외부 스토리지에 올리지 않아도 된다. 외장 SSD를 붙여 저장 공간도 넉넉하다.
  • 오래 걸리는 일에 제한이 없다. 이미지 수십 장을 다시 그리거나, 외부 API를 기다리거나, 몇 분씩 걸리는 작업도 실행 시간 제한 없이 돈다.

대가:

대가대응
기동이 느리다(수십 초)배포 상태 확인을 넉넉히 기다린다(최대 2분)
하드웨어·전원·집 네트워크 장애10분마다 바깥에서 상태를 확인하고, 죽으면 알림
내가 직접 관리해야 한다배포·백업·복구를 스크립트로 만들어 손으로 하는 일을 줄인다
본체 저장 공간이 작다DB와 파일은 외장 SSD에 두고, 배포 전에 SSD가 붙어 있는지 확인한다

앱은 두 가지 방식으로 돌린다. 단순한 앱은 jar 하나를 사용자 서비스(systemd)로 띄우고, DB와 함께 도는 앱은 Docker Compose로 묶는다.

3. 집 서버로 들어오는 길: Tunnel과 Tailscale을 나눈 이유

집 서버에 닿는 길은 셋뿐이고, 공유기 포트는 하나도 열지 않는다.

누가어떤 길로왜
방문자Cloudflare Tunnel내 도메인, HTTPS, 집 IP 숨김, 공격 차단. 집 서버가 바깥으로 먼저 연결을 맺으니 들어오는 포트가 필요 없다
나 (Mac·휴대폰)TailscaleSSH, 관리 화면을 사설망 안에서만. 어디에 있든 같은 주소
GitHub ActionsTailscale (임시 노드)배포할 때만 잠깐 사설망에 들어왔다가 자동으로 빠진다

두 도구 모두 "포트를 안 열고 집 서버에 닿는" 도구지만 기본값이 반대다. Cloudflare Tunnel은 기본이 공개, Tailscale은 기본이 비공개다. 그래서 공개할 것은 Tunnel로, 공개하지 않을 것은 Tailscale로 나누면 실수로 관리 화면이 인터넷에 열리는 일이 구조적으로 줄어든다.

예외가 하나 있다. 외부 플랫폼이 내 서버의 이미지를 직접 내려받아야 하는 경우다. 이때는 Tailscale Funnel로 이미지 경로 하나만 열고, 앱에서도 "Funnel로 들어온 요청은 그 경로만 통과"하도록 한 번 더 막는다. 주소에는 추측할 수 없는 일회용 토큰을 넣고, 작업이 끝나면 닫는다.

4. CI/CD: 푸시하면 끝, 실패하면 되돌리기

서버리스 웹앱

  • 푸시 = 배포. 호스팅이 저장소를 보고 있다가 빌드하고 배포한다.
  • 테스트를 빌드 명령에 넣었다. 로직 테스트가 하나라도 실패하면 빌드가 멈추고, 깨진 코드는 운영에 나가지 않는다.
  • GitHub Actions로 따로 도는 일: 화면 테스트(Playwright), 주간 DB 백업. 백업은 git 커밋이 아니라 기간이 지나면 지워지는 아티팩트에 담는다. 개인정보는 백업에서 뺀다.
  • DB 변경은 SQL 파일로. 마이그레이션 파일에 번호를 붙여 쌓고, 이미 적용한 파일은 고치지 않는다. 파일 머리말에 "코드 배포 전/후 어느 쪽에 실행하는지"를 적는다.

라즈베리파이 앱

GitHub Actions가 푸시마다 이 순서로 돈다.

테스트 → jar 빌드 → Tailscale 임시 접속 → 파일 전송
       → 이전 버전 백업 → 교체 · 재시작 → 상태 확인 (5초 간격, 최대 2분)
       → 실패하면 백업으로 되돌리고 다시 시작
yaml
- name: Connect to Tailscale uses: tailscale/github-action@v3 with: authkey: ${{ secrets.TS_AUTHKEY }} # 임시 키: 작업이 끝나면 노드가 사라진다
  • SSH 키와 Tailscale 키는 GitHub Secrets에만 두고, 작업이 끝나면 러너에서 키 파일을 지운다.
  • 동시 배포 막기. 같은 그룹의 배포가 겹치지 않게 concurrency를 건다.
  • 10분마다 상태 확인. 별도 워크플로가 바깥에서 서비스를 불러 보고, 응답이 없으면 GitHub 이슈를 자동으로 열고, 살아나면 자동으로 닫는다. 내가 자는 동안 죽어 있는 시간을 줄인다.

Docker Compose로 도는 앱은 Mac에서 배포 스크립트를 실행한다. 스크립트는 테스트가 통과해야만 진행하고, 외장 SSD가 붙어 있는지와 서버에 환경 파일이 있는지 먼저 확인한 뒤, 진행 중인 작업이 끝날 때까지 기다렸다가 컨테이너를 다시 띄운다. 서버가 켜질 때는 끊긴 작업을 "다시 시도" 상태로 바꾼다.

5. 비밀값과 백업

무엇어디에
웹앱의 비밀 키호스팅 환경변수 (대시보드에서도 값이 보이지 않는 방식)
집 서버의 비밀값서버의 환경 파일, 권한 600
CI가 쓰는 키GitHub Secrets, 임시 키
브라우저에 들어가는 키공개 키만. 무엇을 할 수 있는지는 DB 권한이 정한다

로그와 오류 메시지에 토큰이 섞이지 않게 하고, git 기록 전체에서 키 형태의 값이 없는지 한 번씩 확인한다. 집 서버의 DB 백업은 권한을 잠근 폴더에 쌓는다.

6. 비용

항목비용
도메인연 몇만 원 (도메인 수에 따라)
DNS · 프록시 · Tunnel (Cloudflare)0원
사설망 (Tailscale 개인 플랜)0원
웹앱 호스팅 · Supabase0원 (무료 플랜 한도 안)
CI (GitHub Actions)0원 (공개·개인 저장소 무료 한도 안)
라즈베리파이이미 있는 기기 + 전기료

회고

잘한 것

  1. "일의 모양"으로 나눈 것. 프로젝트가 아니라 일의 성격(트래픽, 실행 시간, 파일, 공개 여부)으로 둘 곳을 정하니, 새 프로젝트를 시작할 때 고민이 짧아졌다.
  2. 공개 통로와 비공개 통로를 도구 단위로 나눈 것. 기본값이 공개인 도구와 비공개인 도구를 섞지 않으니, 설정 실수 하나로 관리 화면이 열리는 일이 없다.
  3. 자동 롤백과 상태 확인. 혼자 운영할 때 가장 무서운 건 "내가 모르는 사이에 죽어 있는 것"인데, 그걸 코드로 막았다.

다음에 할 것

  • 집 서버의 앱들도 배포 방식을 하나로 맞추기 (지금은 jar 직접 실행과 Docker Compose가 섞여 있다)
  • 백업을 집 밖에도 한 벌 두기
  • 모니터링을 상태 확인 하나에서 응답 시간, 디스크 사용량까지 늘리기

마치며

개인 프로젝트 인프라는 "제일 좋은 도구"보다 "내가 혼자 계속 관리할 수 있는 구조"가 중요했다. 도구는 많아 보이지만, 결국 질문은 세 개였다. 이 일은 어디서 도는 게 맞나, 누가 여기에 닿아야 하나, 실패하면 누가 되돌리나.