GCP Free Tier 만료 D-25, 친구 NAS + Tailscale Funnel 로 호스팅 비용 0원 만들기 — POP-SPOT v1.2
GCP Free Tier 만료를 앞두고 친구 시놀로지 NAS 의 Proxmox · Ubuntu VM 으로 이전. nginx+certbot 대신 Tailscale Funnel 한 줄로 HTTPS 자동 발급. 6 시간 동안의 19 가지 트러블슈팅.
GCP Free Tier 가 5/28 에 만료된다는 메일을 받고 옮길 곳을 찾다가, 친구 시놀로지 NAS 위에 빈 VM 한 칸을 빌렸다. "한두 시간이면 옮기겠지" 라고 시작했는데 실제로는 여섯 시간 걸린 이야기다.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| Proxmox | NAS · 일반 PC 에 깔 수 있는 가상화 OS. 그 위에 가상 머신을 여러 개 띄울 수 있다 |
| Tailscale Funnel | 내 집의 서버를 외부 인터넷에 HTTPS 로 노출해주는 기능. 인증서 자동 갱신까지 포함 |
| Let's Encrypt | 무료 HTTPS 인증서 발급 기관. certbot 명령으로 90 일짜리 인증서를 받아 쓴다 |
| systemd | 리눅스에서 "이 명령을 부팅하자마자 백그라운드로 띄워줘" 를 관리해주는 표준 도구 |
| EnvironmentFile | systemd 가 서비스 띄울 때 읽는 환경변수 파일 (KEY=VALUE) |
| OWNER (PostgreSQL) | DB 객체의 소유자. INSERT/ALTER 권한의 기준이 된다 |
무엇이 바뀌었나
| 분류 | v1.1 | v1.2 |
|---|---|---|
| 호스팅 | GCP VM · 월 ~$30 (Free Tier 만료 예정) | 친구 시놀로지 NAS · Proxmox · Ubuntu VM · 월 0원 |
| 외부 노출 | nginx + certbot | Tailscale Funnel 한 줄 |
| 도메인 | popspot.duckdns.org | vm-113.tailc57dd4.ts.net • popspot.co.kr |
| 배포 | <code>start.sh • nohup</code> | systemd 서비스 + EnvironmentFile |
| 스택 | Spring Boot 3.x · PG 14 | Spring Boot 4.0.2 · PG 14 · Redis 6 |
| 부팅 시간 | 약 47 초 | 약 9 초 (메모리 여유 + JVM 옵션 정리) |
왜 이렇게 했음
호스팅 0원 — GCP Free Tier 가 5/28 에 끝났다. 결제 카드 등록하면 e2-micro 정도는 계속 무료지만, 카드 등록은 미루고 싶었음. 친구가 시놀로지 NAS 에 빈 VM 한 칸을 빌려준다고 해서 그쪽으로 옮기기로 했다.
nginx 제거 — 옛 서버에는 nginx + certbot 으로 HTTPS 를 발급해 썼다. 새 NAS 에서는 같은 작업을 다시 해야 하는데, 인증서 발급에 80/443 포트가 필요하고 그건 친구 공유기를 건드려야 한다. 친구 공유기는 건드리지 않기로 했음. Tailscale Funnel 은 인증서 자동 + 포트포워딩 자동이라 한 줄로 끝났다.
Docker 안 쓰기 — Docker 가 좋은 도구인 건 알지만, GCP 시절의 환경을 그대로 옮기는 게 가장 빠른 길이었다. 그리고 "한 명이 디버깅하는 프로젝트" 라 컨테이너 한 겹이 더 끼면 로그 보러 들어가는 단계가 길어진다. systemd + jar 직실행이 단순함.
코드로는 어떻게 (필요한 부분만)
systemd 서비스 파일 — 이걸 만들어두면 서버 부팅하자마자 백엔드가 같이 뜬다.
# v1.2 — /etc/systemd/system/popspot.service
# systemd 가 읽을 수 있는 표준 INI 형식. 세 섹션으로 분리됨.
[Unit]
Description=POP-SPOT backend # systemctl status 에 표시될 한 줄 설명
After=network.target # 네트워크 초기화 끝난 다음 시작
# ^^^^^^^^^^^^^^^^^^^^^^ (network.target 이 Failed 이어도 여기 시작됨 — 순서만 규정)
[Service]
Type=simple # ExecStart 실행되면 즉시 "실행 중" 으로 판단
User=reo4321 # root 아닌 일반 계정으로 실행 — 익스플로이트 피해 최소화
WorkingDirectory=/home/reo4321/popspot
# ^^^^^^^^^^^^^^^^^^^^^ 상대경로 로그/파일 쓰면 이 경로 기준
EnvironmentFile=/home/reo4321/popspot/popspot.env
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ KEY=VALUE 파일. 시크릿 원천
# chmod 600 필수 (다른 사용자 읽기 차단)
ExecStart=/usr/bin/java -jar /home/reo4321/popspot/app.jar
# ^^^^^^^^^^^^^ 절대경로 필수 — systemd 는 PATH 안 읽음
Restart=on-failure # 비정상 종료 (exit ≠0) 일 때만 재시작
# ^^^^^^^^^^^^^ always 는 kill 해도 재도전 — 디버그 방해되므로 피함
[Install]
WantedBy=multi-user.target # systemctl enable 시 이 그룹에 연결
# ^^^^^^^^^^^^^^^^^^^^ multi-user = 일반적인 부팅 완료 상태Tailscale Funnel — 정말 한 줄로 끝난다.
파일: 코드 파일 아닄. 서버의 터미널에서 직접 입력하는 솨 명령
# v1.2 — Tailscale Funnel 로 외부 HTTPS 노출
sudo tailscale funnel --bg 8080
# ^^^^^^^^^ ^^^^ ^^^^
# | | |
# | | +-- 로컬에서 실행 중인 포트 (Spring Boot 기본 8080)
# | +------ 백그라운드 실행. 터미널 닫아도 유지
# +-------------------------- tailscale CLI (sudo 필요 — 시스템 상태 변경)
#
# 이 한 줄이 대체한 것:
# 1. 공유기 포트포워딩 80·443 (내 집 공유기 만지고 싶지 않음)
# 2. nginx 리버스 프록시 설정
# 3. certbot 으로 Let's Encrypt 90일용 인증서 발급 + 갱신 크론
# 4. ufw·iptables 방화벽 규칙
#
# 실행 직후 vm-113.tailc57dd4.ts.net 형태 도메인이 자동 할당
# popspot.co.kr 은 다음 단계에서 CNAME 으로 이 도메인을 가리키게 연결DB 옮길 때 pg_dump 의 --no-owner 옵션이 사고를 만든다. 새 서버에서 객체가 전부 postgres 소유로 복원되어 popspot_user 가 INSERT 못 함.
파일: psql / DataGrip 에서 직접 실행한 쿼리 (코드 파일 아닌 DB 원키 타임 복구 용)
-- v1.2 사고 재현 — pg_dump --no-owner 로 덤프/복원 후 발생
ERROR: permission denied for table popup_store
-- ^^^^^^^^^^^^ 모든 테이블의 OWNER 가
-- 복원 수행한 postgres 로 바뀌어
-- popspot_user 가 INSERT/UPDATE 명령 거부당함파일: 같은 psql 세션에서 이어서 실행하는 PL/pgSQL 블록
-- v1.2 수정 — PL/pgSQL 로 모든 테이블 OWNER 한번에 교체
DO $$
-- ^^ 익명 코드 블록 시작. 달러 기호($$) 로 감싸서 바깥 쪽 따옴표 이스케이프 회피
DECLARE r record;
-- ^^^^^^^^^ 행 하나를 담을 동적 타입 변수. 컬럼 구조는 루프에서 결정
BEGIN
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname='public' LOOP
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- public 스키마의 모든 사용자 테이블을 한 행씩 가져옴
-- (pg_tables 는 PostgreSQL 시스템 카탈로그 뷰)
EXECUTE 'ALTER TABLE public.' || quote_ident(r.tablename)
-- ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
-- | |
-- | +-- 테이블명에 특수문자 있을 경우 안전하게 인용 (SQL 인젝션 방지)
-- +-- 동적 SQL 조립. EXECUTE 는 문자열로 만들어 실행
|| ' OWNER TO popspot_user';
-- ^^^^^^^^^^^^^^^^^^^^^ 해당 테이블의 소유자를 popspot_user 로 이전
-- popspot_user 가 이제부터 자유롭게 DML 실행 가능
END LOOP;
END $$;
-- 시퀀스·함수도 같은 패턴으로 따로 돌려야 함 (pg_sequences, pg_proc)비하인드 · 사고
옮기면서 한 시간 이상 잡아먹은 함정 몇 개.
교훈 한 줄. 옮기는 일은 코드보다 환경 (계정명, 경로, 키 이름, 콘솔의 콜백 등) 에서 문제가 더 많이 터진다.