saip.learning notes

saip 블로그 프로젝트: 진행하며 알게 된 것

프로젝트 · 마지막 수정 2026-10-24

이 노트는

블로그(saip)를 만들면서 직접 부딪혀서 알게 된 것을 모은다. 개념 설명은 주제별 노트에 있고, 여기는 "다음에 비슷한 걸 하면 미리 알았으면 좋았을 것" 위주다. 프로젝트를 진행할 때마다 아래에 계속 덧붙인다.

Note

왜 만들었는지와 설계 결정은 이 블로그를 만든 이유와 설계 결정에 있다.

한눈에 보기

단계 한 것 자세한 노트
계획 1 로컬 블로그, /blog 스킬, 비밀값 검사 Flyway
계획 2 Google 로그인, 관리자 편집기, 댓글, 버전 복원, 이미지 업로드 Google 로그인, 로그인 댓글, 웹 보안, 이미지
계획 3 Docker, Lightsail, 도메인·HTTPS, GitHub Actions, S3 백업 Docker, Lightsail, 도메인, Actions, 백업
flowchart LR
  pc["`**내 PC**
  Claude Code + /blog`"] -- git push --> gh["`**GitHub Actions**
  test → image → deploy`"]
  gh --> ghcr@{ shape: cyl, label: "ghcr 이미지" }
  gh -- SSH --> srv["`**Lightsail**
  Caddy → Spring → PostgreSQL`"]
  ghcr -.-> srv
  pc -- 노트 API --> srv
  srv -- 매일 04:00 --> s3@{ shape: cyl, label: "S3 백업" }
  visitor([방문자]) -- https --> srv
  class ghcr,s3 ext
  classDef ext stroke-dasharray: 4 3

일하는 방식: AI와 같이 개발할 때

  • 설계 문서 → 계획 → 작은 작업 단위로 나눴다. 작업마다 먼저 개념 설명을 듣고 "진행해"로 시작했다. 그냥 맡기면 결과는 나오지만 내가 배운 게 남지 않는다.
  • 테스트 먼저(TDD): 실패하는 테스트를 먼저 보고 고친다. "고쳤다"는 말 대신 테스트 결과로 확인한다.
  • 진행 기록(ledger): 대화가 길어지면 앞 내용이 요약되면서 잊힌다. 어디까지 했는지, 계획과 다르게 정한 것(왜, 틀리면 뭘 잃는지)을 파일에 남겨 두니 이어서 하기 쉬웠다.
  • 결정은 내가: 계정 만들기, 결제, 공개 여부(저장소·패키지 공개), 리전, 도메인 같은 건 AI가 대신하지 않고 선택지를 받아 내가 정했다.
  • 비밀값은 대화창에 붙여 넣지 않는다: 키는 서버의 aws configure, gh secret set, 사용자 환경변수에만 넣었다. 화면에는 "설정됨(True/False)"만 확인.

계획 1~2: 앱 만들기

겪은 것 원인 해결
환경변수를 설정했는데 인식 안 됨 프로그램은 켜질 때 환경변수를 복사해 간다 터미널 앱 전체를 다시 켠다
한글 slug로 요청하면 실패 HTTP 요청 줄은 ASCII만 된다 urllib.parse.quote로 퍼센트 인코딩
토큰이 다른 호스트로 샐 수 있음 Python urllib은 리다이렉트할 때 Authorization을 그대로 넘긴다 리다이렉트 거부, localhost 외에는 https만
노트가 없는 카테고리도 트리에 보임 엔티티의 양쪽 연관 목록 중 한쪽만 갱신 변경 메서드에서 양쪽 목록을 같이 맞춤
긴 글을 쓰다 저장하면 로그인 풀림 세션 기본 30분 server.servlet.session.timeout: 7d

계획 3: 배포에서 알게 된 것

Docker와 compose

  • 개발용 compose.yaml과 운영용 compose의 프로젝트 이름이 같으면(폴더 이름 기준) 컨테이너와 볼륨이 섞인다. 운영용에 name: saip-prod를 명시했다.
  • docker images의 크기(522MB)는 압축 풀린 크기고, 레지스트리에 올라가는 건 압축본(149MB)이다.
  • ${VAR:?}: 값이 없으면 compose가 바로 멈춘다. 빈 비밀번호로 뜨는 사고를 막는다.

GitHub과 ghcr

  • 저장소를 공개하기 전에 커밋 이메일을 확인했다. 개인 메일이 기록에 남아 있어서 GitHub noreply 주소로 전부 다시 썼다(되돌릴 백업 브랜치는 로컬에만 둠).
  • 테스트용 가짜 결제 키 문자열 때문에 GitHub push protection이 push를 막았다. 진짜 키가 아니어도 형식이 같으면 막는다. 문자열을 "sk_live_" + "..."처럼 쪼개서 해결.
  • ghcr 이미지 이름은 소문자만 된다(Limsangkeun → limsangkeun).
  • 패키지가 비공개면 서버의 docker pull이 unauthorized. 공개로 바꾸는 건 웹 화면에서만 된다.
  • gh auth refresh -s write:packages처럼 토큰 권한(scope) 을 따로 추가해야 하는 경우가 있다.

Windows에서 작업할 때의 함정

  • Git Bash가 /로 시작하는 인자를 Windows 경로로 바꾼다. gh api /user가 C:/Program Files/Git/user가 됐다. 앞의 /를 빼거나 MSYS_NO_PATHCONV=1.
  • PowerShell 파이프는 BOM을 붙인다. 파이프로 넘긴 JSON이나 비밀값 앞에 보이지 않는 문자가 붙었다. Bash에서 tr -d '\r'로 처리.
  • 줄바꿈(CRLF): 서버에서 실행할 *.sh, deploy/**, Dockerfile은 .gitattributes에 eol=lf.
  • 실행 권한: Windows에서 만든 gradlew가 git에 100644로 들어가 있어서 리눅스 러너에서 Permission denied. git update-index --chmod=+x gradlew.
  • 한글 Windows의 nslookup 출력은 한글이라 grep이 깨졌다. DNS 확인은 DNS-over-HTTPS JSON(https://cloudflare-dns.com/dns-query?name=...&type=A)으로.
  • SSH 명령 안에 따옴표가 여러 겹이면 깨진다. 스크립트 파일을 scp로 올려서 실행하는 게 편하다.

서버, 도메인, HTTPS

  • 무료 체험 리전이 시드니로 고정이라 그대로 썼다. 한국에서 약간 느리지만 블로그엔 충분하다.
  • 1GB 메모리에 Spring + PostgreSQL + Caddy를 올리려면 스왑 2GB와 컨테이너별 메모리 제한, JVM -Xmx가 필요했다.
  • Cloudflare에서 도메인을 사면 A 레코드를 DNS only(회색 구름) 로 둬야 Caddy가 직접 인증서를 받는다.
  • 프록시(Caddy) 뒤의 Spring은 자기가 http로 받는 줄 안다. server.forward-headers-strategy: framework가 없으면 Google 로그인의 redirect_uri가 http가 된다.
  • 같은 이유로 세션 쿠키에 Secure가 빠져 있었다. 운영 점검에서 발견하고 SERVER_SERVLET_SESSION_COOKIE_SECURE=true.

자동 배포와 운영

  • 이미지를 :latest로만 올리면 예전 버전은 태그 없는 이미지가 되어 되돌릴 수 없다. 커밋 SHA 태그를 같이 남긴다.
  • GitHub Action은 태그(@v7.0.1)가 아니라 커밋 SHA로 고정한다. 태그는 옮겨질 수 있다.
  • 배포마다 이미지가 서버에 쌓였다(7개, 1.38GB). 배포 끝에 docker image prune -af.
  • 배포 중 15~20초는 502. 컨테이너가 하나라서다. 무중단은 블루-그린 배포가 필요하다(메모리 두 배).
  • 백업 키는 올리기만 되게(s3:PutObject). 서버가 털려도 백업을 지울 수 없다.
  • 백업은 복원해 봐야 백업이다. 실제로 받아서 로컬에 복원하고 운영과 화면을 비교했다.

비밀값 검사가 실제로 잡은 것

노트를 올리다가 로컬 검사에 한 번 걸렸다. 테스트 예시로 적은 비밀번호=값 형태의 줄이었다. 진짜 비밀값은 아니었지만, 검사가 제대로 작동한다는 확인이 됐다. 문장으로 바꿔서 다시 올렸다.

다음 프로젝트에 가져갈 체크리스트

  • 저장소 공개 전 커밋 이메일 확인(git log --format='%ae' | sort -u)
  • .gitattributes로 서버용 파일 LF 고정, 스크립트 실행 권한 확인
  • 비밀값은 .env(chmod 600)와 GitHub Secrets에만. 저장소엔 .env.example
  • 프록시 뒤 앱이면 forwarded 헤더와 Secure 쿠키 확인
  • 이미지 태그는 SHA, Action은 SHA 고정
  • 배포 전용 SSH 키 따로, known_hosts로 서버 확인
  • 백업 키는 쓰기 전용, 복원 리허설까지
  • 운영 점검표: docker compose ps, 로그, free -m, df -h, docker system df, 인증서 만료일

업데이트 기록

댓글 0

로그인하고 댓글 달기