백엔드트러블슈팅갈아엎기 시리즈

GCP Free Tier 만료 D-25, 친구 NAS + Tailscale Funnel 로 호스팅 비용 0원 만들기 — POP-SPOT v1.2

김동현
··5분 읽기

GCP Free Tier 만료를 앞두고 친구 시놀로지 NAS 의 Proxmox · Ubuntu VM 으로 이전. nginx+certbot 대신 Tailscale Funnel 한 줄로 HTTPS 자동 발급. 6 시간 동안의 19 가지 트러블슈팅.

GCP Free Tier 가 5/28 에 만료된다는 메일을 받고 옮길 곳을 찾다가, 친구 시놀로지 NAS 위에 빈 VM 한 칸을 빌렸다. "한두 시간이면 옮기겠지" 라고 시작했는데 실제로는 여섯 시간 걸린 이야기다.

이 글에서 다루는 것

  • GCP Compute Engine 에서 친구 NAS 의 Proxmox / Ubuntu VM 으로 이전한 전 과정
  • nginx + Let's Encrypt 조합을 Tailscale Funnel 한 줄로 대체한 이유
  • Docker 를 끼우지 않고 systemd + EnvironmentFile 만 쓰기로 한 결정
  • 옮기는 동안 만난 19 가지 트러블슈팅 중 사람들이 흔히 빠지는 것 몇 개
  • 결과: 월 ~$30 → 월 0원, 부팅 47 초 → 9 초
  • 모르는 단어 한 줄로

    용어한 줄 설명
    ProxmoxNAS · 일반 PC 에 깔 수 있는 가상화 OS. 그 위에 가상 머신을 여러 개 띄울 수 있다
    Tailscale Funnel내 집의 서버를 외부 인터넷에 HTTPS 로 노출해주는 기능. 인증서 자동 갱신까지 포함
    Let's Encrypt무료 HTTPS 인증서 발급 기관. certbot 명령으로 90 일짜리 인증서를 받아 쓴다
    systemd리눅스에서 "이 명령을 부팅하자마자 백그라운드로 띄워줘" 를 관리해주는 표준 도구
    EnvironmentFilesystemd 가 서비스 띄울 때 읽는 환경변수 파일 (KEY=VALUE)
    OWNER (PostgreSQL)DB 객체의 소유자. INSERT/ALTER 권한의 기준이 된다

    무엇이 바뀌었나

    분류v1.1v1.2
    호스팅GCP VM · 월 ~$30 (Free Tier 만료 예정)친구 시놀로지 NAS · Proxmox · Ubuntu VM · 월 0원
    외부 노출nginx + certbotTailscale Funnel 한 줄
    도메인popspot.duckdns.orgvm-113.tailc57dd4.ts.netpopspot.co.kr
    배포<code>start.sh • nohup</code>systemd 서비스 + EnvironmentFile
    스택Spring Boot 3.x · PG 14Spring 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 서비스 파일 — 이걸 만들어두면 서버 부팅하자마자 백엔드가 같이 뜬다.

    plain text
    # 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 — 정말 한 줄로 끝난다.

    파일: 코드 파일 아닄. 서버의 터미널에서 직접 입력하는 솨 명령

    bash
    # 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 원키 타임 복구 용)

    sql
    -- v1.2 사고 재현 — pg_dump --no-owner 로 덤프/복원 후 발생
    ERROR: permission denied for table popup_store
    --                              ^^^^^^^^^^^^ 모든 테이블의 OWNER 가
    --                                            복원 수행한 postgres 로 바뀌어
    --                                            popspot_user 가 INSERT/UPDATE 명령 거부당함

    파일: 같은 psql 세션에서 이어서 실행하는 PL/pgSQL 블록

    sql
    -- 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)

    비하인드 · 사고

    옮기면서 한 시간 이상 잡아먹은 함정 몇 개.

  • SSH 사용자명 — GCP 의 습관으로 ubuntu@vm 으로 접속했더니 Permission denied. 시놀로지 Ubuntu 의 권장 계정명이 reo4321 이었다. xshell 세션 메모를 확인하니 거기 적혀 있었음.
  • nano 화살표.env 에 값을 붙여 넣다가 nano 안에서 화살표 키를 잘못 눌러 ANSI escape 문자 (예: \x1b[3~) 가 그대로 들어갔다. 환경변수 이름이 한 글자만큼 깨져 부팅 실패. 단축키 (Ctrl+K, Ctrl+O, Ctrl+X) 만 쓰거나 vim 으로 옮겨야 함.
  • Vercel 환경변수 이름 — 코드는 NEXT_PUBLIC_API_URL 을 찾는데 Vercel 에는 NEXT_PUBLIC_API_BASE_URL 로 등록되어 있었다. 결과: localhost 폴백으로 호출이 가서 OAuth 가 다 깨짐.
  • OAuth 콜백 URL 3 곳 — 카카오 · 네이버 · 구글 콘솔에 각각 새 도메인을 추가해야 했다. 구 도메인도 일주일 정도 같이 두는 게 안전. 북마크 옛 주소로 들어오는 사용자 대응이다.
  • Sentry CLI — Windows 빌드 단계에서 Defender 가 Sentry 의 sourcemap 업로드 CLI 를 차단함. -x sentryBundle* 플래그로 빼고 운영 서버에서만 실행하게 분리했다.
  • 교훈 한 줄. 옮기는 일은 코드보다 환경 (계정명, 경로, 키 이름, 콘솔의 콜백 등) 에서 문제가 더 많이 터진다.


    관련 글

  • 이전 — v1.1, 보안 빚 한 번에 갚기
  • 다음 — v1.3, 결제 페이지 폐기하고 음악 매칭을 새 코어로
  • 공유

    댓글