saip 블로그 프로젝트: 진행하며 알게 된 것
이 노트는
블로그(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, 인증서 만료일
업데이트 기록
- 2026-10-24: 계획 1~3 완료 시점에 처음 작성. 운영: https://saip.nexus
댓글 0
로그인하고 댓글 달기