처음 목표는 단순했다. Yohaku 테마와 Mix Space Core를 사용해 오랫동안 글을 쓸 수 있는 개인 사이트를 만드는 것이었다.
최종 구성
현재 구조는 다음과 같다.
이미지에서 생략한 두 가지를 일부러 분리했다: 콘텐츠는 항상 Mix Space 관리자에서 유지하고, 인프라와 소스 코드는 Git과 CI로 관리한다. 이렇게 하면 글을 쓸 때 이미지를 다시 빌드할 필요가 없고, 프런트엔드를 수정할 때만 배포 프로세스가 실행된다.
현재 구조는 다음과 같다.
8cat.life: Yohaku 프런트엔드, Tencent Cloud Lightweight Server의 Docker 컨테이너에서 실행된다.api.8cat.life: Mix Space Core, Caddy가 Core 서비스로 리버스 프록시한다.- Core 의존성: PostgreSQL, Redis, 객체 스토리지 COS.
- 프런트엔드 이미지: CNB에서 Docker 이미지를 빌드하고, Tencent Cloud 개인 이미지 레지스트리 TCR로 푸시한다.
- 자동 배포: CNB는 Tencent Cloud TAT를 통해 Lightweight Server의 자동화 에이전트를 호출하여 지정된 이미지를 가져오고 프런트엔드 컨테이너를 재구축한다.
핵심은 '이미지 빌드'와 '이미지 실행'을 분리하는 것이다: CNB는 빌드를 담당하고, 서버는 이미 빌드된 버전을 가져와 실행만 한다.
지나온 과정
1. 처음에는 EdgeOne Pages에 직접 배포하려 했다
Yohaku는 Next.js SSR 프로젝트다. EdgeOne Pages는 Next.js를 인식하지만, 실제 빌드 시 Next.js 16의 Turbopack/webpack 프로세스가 플랫폼의 메모리 제한으로 강제 종료된다: 로그에서 SIGKILL과 약 6 GiB의 메모리 상한을 확인할 수 있다.
빌드를 Turbopack에서 webpack으로 전환해도 빌드는 여전히 종료된다. 또한 EdgeOne 런타임에서 해당 프로젝트의 middleware/proxy 호환성 문제도 발생했다. 그래서 프로덕션 브랜치에서 라우팅을 억지로 수정해 플랫폼에 맞추는 작업은 하지 않았다.
결정: EdgeOne은 정적 사이트와 가벼운 프레임워크에 적합하지만, 현재 Yohaku의 SSR 빌드 규모는 이 경로에 맞지 않는다. 테스트 브랜치는 유지하고, 프로덕션 환경은 Docker로 전환했다.
2. GitHub Actions는 빌드할 수 있지만 최종 배포 파이프라인으로는 부적합하다
GitHub Actions는 Yohaku를 성공적으로 빌드했으며, 소스 코드, 서브모듈, Next.js 빌드 자체에는 근본적인 문제가 없음을 보여준다.
하지만 이미지를 GHCR에 올린 후 Tencent Cloud 서버가 해외 이미지를 가져오는 속도가 느렸다. 또한 GitHub Runner가 서버에 직접 SSH로 접속해 배포하면, 익숙하지 않은 IP 경고와 장기 자격 증명 관리 문제가 발생한다.
결정: GitHub는 소스 코드의 메인 저장소로 유지하고, 이미지 빌드와 서버 배포는 Tencent Cloud 생태계 내에서 완료한다.
3. CNB, TCR, TAT로 자동 배포 구축
최종적으로 CNB로 Docker 빌드를 수행하고, 결과물을 프라이빗 TCR로 푸시했다. 이미지에는 변경 불가능한 커밋 SHA 태그와 latest를 함께 사용하며, 서버는 실제로 SHA 태그를 배포하여 '웹사이트가 어떤 커밋을 실행 중인지' 추적할 수 있게 했다. 배포는 '서버가 코드를 다시 컴파일하는 것'이 아니라, 이미 검증된 완성 이미지를 가져오는 것이다.
이 과정에서 몇 가지 문제를 겪었다:
- CNB가 npm 중국 미러를 사용할 때 일부 패키지가 미러 사이트에 아직 동기화되지 않아 404가 발생했다. 이런 패키지는 공식 npm 레지스트리로 폴백해야 했다.
- Docker 빌드 중
git: not found는 설치 단계의 hooks에서 발생했다. 컨테이너 빌드에는.git디렉터리가 없으므로 무해한 노이즈이며, 빌드 실패 원인이 아니다. - TAT에서 처음에 잘못된 CVM 스타일 인스턴스 ID를 사용했다. Lightweight Server는 실제로
lhins-...형식의 인스턴스 ID가 필요하다. - TAT CAM 권한은
RunCommand외에도 invocation 및 작업 상태 조회 권한이 필요하다. 하나라도 없으면 '명령이 전송되었지만 파이프라인이 여전히 실패'한다.
결국 CNB 빌드가 완료되면 TAT를 호출하여 서버가 다음 작업을 수행한다: 지정된 SHA의 이미지 가져오기, compose 서비스 업데이트, yohaku 컨테이너 재구축.
콘텐츠 및 테마 설정
Mix Space의 '페이지'는 일반 CMS 콘텐츠이지 Yohaku 테마 파일이 아니다. 따라서 페이지 하단의 /about, /about-site, /message 링크가 처음에 404가 된 이유는 간단하다: 해당 페이지가 아직 생성되지 않았기 때문이다.
현재 생성된 페이지:
- 소개:
/about - 사이트 소개:
/about-site - 방명록:
/message
Yohaku의 홈페이지와 페이지 하단 시각적 설정은 관리자 '코드 조각'의 theme/shiro JSON에 있으며, 프로젝트 루트의 theme.json이 아니다. 여기서 홈페이지 제목, 소개, 한 줄 문구, favicon, 페이지 하단 링크를 구성한다.
이미지, favicon, 이메일
- 이미지는 Tencent Cloud COS를 사용한다. Core는 현재 하나의 S3 연결 구성을 사용하며, 블로그 본문 이미지와 댓글 이미지는 서로 다른 객체 경로 접두사로 분리된다. 액세스 키는 해당 스토리지 버킷에 필요한 최소 권한만 부여된다.
- favicon은 이전 사이트에서 마이그레이션했으며, 프런트엔드는 브라우저가 확대/축소할 수 있도록 512px 원본 이미지를 제공한다. 테마에 이전 경로가 유지되면, 프런트엔드는 이 고해상도 리소스로 호환성 폴백을 수행한다.
- 이메일 구독은 Resend를 사용한다. 발신 도메인은
updates.8cat.life이며, 예를 들어 발신 주소는newsletter@updates.8cat.life다. API 키는 Core 관리자 설정에만 저장되며, 프런트엔드나 코드 저장소에는 들어가지 않는다.
여전히 기억해야 할 제한 사항
Google 댓글 로그인
Google OAuth의 브라우저 인증은 완료할 수 있지만, 서버는 인증 코드를 토큰으로 교환하기 위해 oauth2.googleapis.com에 접근해야 한다. Tencent Cloud 서버 내 Core 컨테이너에서 해당 주소의 443 포트 연결이 시간 초과되어 결국 invalid_code가 표시된다.
이것은 Google 콜백 주소 누락이 아니다. Google 로그인을 활성화하려면 Core가 Google에 안정적으로 접근할 수 있는 HTTPS 프록시를 구성해야 한다. 현재는 이미 사용 가능한 GitHub 로그인을 유지하는 것이 더 적절하다.
import { createServer } from 'node:http'
const server = createServer() // [!code highlight]
server.listen(3000)
console.log('ready')