정적 홈페이지(www.trr.co.kr)와 블로그(blog.trr.co.kr)를 최소한의 인프라로 운영하는 단일 레포입니다.
프로덕션에서는 홈페이지/블로그를 호스트 Nginx가 정적으로 서빙하고, OAuth만 컨테이너로 유지합니다.
- 운영 가이드:
docs/OPERATIONS.md src/— 홈페이지 정적 파일(HTML/CSS/JS)Dockerfile— (로컬 개발용) nginx unprivileged, 8080nginx.conf— (로컬 개발용) 보안 헤더/캐시/압축 기본 설정docker-compose.yml— (로컬 개발용)web서비스traum_blog/— Hugo + Decap CMS 블로그(별도 compose/Dockerfile 포함)static/brand/— 회사 로고 등 브랜드 에셋(홈/블로그 공용)
홈페이지 컨테이너 바인딩은 .env로 조정합니다. 예시는 .env.example 참고.
HTTP_BIND_HOST=127.0.0.1
HOMEPAGE_PORT=17201
권장: Admin(/admin)까지 포함한 E2E 개발은 docker compose로 블로그+OAuth를 실행하세요.
웹은 단순 정적 미리보기면 비컨테이너도 가능하지만, Nginx 헤더/캐시 동작까지 검증하려면 web 서비스로 실행합니다.
# 홈페이지 (http://127.0.0.1:17201)
docker compose build web && docker compose up -d web
# 블로그 (http://127.0.0.1:17202)
cd traum_blog
cp static/admin/config.dev.yml static/admin/config.yml
# (필요 시) .env에 OAUTH_TEST_MODE=1 설정 후
docker compose build blog && docker compose up -d blog oauth
# CMS Admin (Decap)
# - 로컬: http://localhost:17202/admin/
# - 운영: https://blog.trr.co.kr/admin/
# - 필드: Title, Date, Draft, Category(select: market|ops|behind|story), Summary, KPIs(list of {label,value}), Description, Tags, Body- 기본 권장:
docker compose up -d blog oauth(Admin/OAuth 포함 전체 플로우 검증) - 빠른 미리보기(웹만):
cd src && python3 -m http.server 17201 --bind 127.0.0.1(또는npx serve) - Nginx 동작까지 검증:
docker compose up -d web
- DNS
www.trr.co.kr→ VPS IPblog.trr.co.kr→ VPS IP
- 정적 루트(Nginx 예시)
server {
listen 80; server_name www.trr.co.kr;
root /srv/www/trr;
index index.html;
}
server {
listen 80; server_name blog.trr.co.kr;
root /srv/traum_homepage/traum_blog/public;
index index.html;
location /oauth/ { proxy_pass http://127.0.0.1:17203/; }
}
- TLS는 certbot 또는 Caddy/Traefik 권장.
- 관리페이지
- 운영:
https://blog.trr.co.kr/admin/ - 로컬:
http://localhost:17202/admin/
- 운영:
- GitHub OAuth App 등록(Homepage/Callback URL은 README의 블로그 섹션 참조)
- 설정 파일 복사
- 로컬 개발:
cp traum_blog/static/admin/config.dev.yml traum_blog/static/admin/config.yml - 운영 배포: 워크플로가 자동으로
static/admin/config.prod.yml를config.yml로 복사합니다 (없을 경우)
- 로컬 개발:
traum_blog/.env.example을 복사해 값 설정 후oauth서비스 기동- 운영/실제 로그인:
DEV_ALLOW_ALL_ORIGINS=0,OAUTH_TEST_MODE=0 - 로컬 테스트 토큰(팝업 없이 즉시 발급)이 필요할 때만
OAUTH_TEST_MODE=1로 전환하고 테스트 후 다시 0으로 되돌리세요. .env수정 후에는docker compose up -d --force-recreate --no-deps oauth로 OAuth 컨테이너를 재시작해야 적용됩니다.
- 운영/실제 로그인:
- E2E 확인:
OAUTH_TEST_MODE=1 npx playwright test(사전에cd tests/e2e && npm install)
- 소스 경로:
traum_blog/oauth/src/(TypeScript, ESM) - 로컬 실행 예시
cd traum_blog/oauth npm install npm run build node dist/server.js # 또는 Compose로: cd traum_blog && docker compose up -d --build oauth
- 환경 변수(요약, 자세한 항목은
traum_blog/.env.example참고)- 필수:
GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET - URL/오리진:
OAUTH_REDIRECT_URL,ALLOWED_ORIGINS - 스코프:
GITHUB_SCOPE=public_repo(공개) 또는repo(비공개) - 동작:
OAUTH_TEST_MODE,DEV_ALLOW_ALL_ORIGINS,OAUTH_AUTOCLOSE,OAUTH_AUTOCLOSE_DELAY_MS,OAUTH_SUCCESS_BURST_* - 메트릭(옵션):
METRICS_ENABLED,METRICS_BASIC_AUTH_USER,METRICS_BASIC_AUTH_PASS
- 필수:
- 보안
- 팝업 응답은 CSP nonce 기반 스크립트를 사용(unsafe-inline 제거)
- 운영에서
/oauth/metrics는 차단 또는 Basic Auth 보호 권장(Nginx 예시 하단 참고)
- 증상: Admin 접속 시 "Failed to load config.yml (404)" 또는 콘솔에 "Response was not yaml" 출력
- 원인:
static/admin/config.yml누락 또는 MIME이 YAML이 아님 - 조치
- 프로덕션: Actions가 자동 생성하지만, 필요 시 수동 생성
cp traum_blog/static/admin/config.prod.yml traum_blog/static/admin/config.yml- Hugo 빌드 후 산출물(
public/admin/config.yml)이 있는지 확인
- Nginx에서 정확한 MIME 고정(운영 vhost에 추가)
# blog.trr.co.kr 서버 블록 내부 location = /admin/config.yml { types { }; default_type text/yaml; try_files $uri =404; }
- 검증:
curl -I https://blog.trr.co.kr/admin/config.yml가200이고Content-Type: text/yaml
- 프로덕션: Actions가 자동 생성하지만, 필요 시 수동 생성
- 저장 위치:
traum_blog/static/brand/ - 프로덕션 참조 URL:
https://blog.trr.co.kr/brand/<파일명>.svg - 로컬 참조 URL:
http://127.0.0.1:17202/brand/<파일명>.svg - 캐시 주의: SVG는 30일 +
immutable→ 교체 시 파일명 버저닝(예:logo.20251028.svg)
블로그는 커밋 시 GitHub Actions가 Hugo로 빌드하고 결과물(traum_blog/public/)만 VPS로 rsync 동기화합니다.
- 워크플로:
.github/workflows/deploy-blog.yml - Secrets(레포 Settings → Secrets and variables → Actions)
- Name
DEPLOY_HOST→ Value서버_IP - Name
DEPLOY_USER→ Valueroot(또는deploy) - Name
DEPLOY_SSH_PORT→ Value22(기본값이면 생략 가능) - Name
DEPLOY_SSH_KEY→ Value (SSH 개인키 전체. 예:-----BEGIN OPENSSH PRIVATE KEY----- ...)- 권장: 배포 전용 키 생성 후 공개키는 VPS
~/.ssh/authorized_keys에 등록 - 예시(로컬/Git Bash):
ssh-keygen -t ed25519 -C "gh-actions-deploy-trr" -f ~/.ssh/gh_actions_trr -N ''ssh-copy-id -i ~/.ssh/gh_actions_trr.pub root@서버_IPssh -i ~/.ssh/gh_actions_trr root@서버_IP 'echo OK'(접속 확인)
- 권장: 배포 전용 키 생성 후 공개키는 VPS
- Name
배포 흐름: CMS에서 글 발행(=커밋) → Actions 자동 실행 → 수십 초 내 반영(컨테이너 재시작 없음).
.env와 인증서는 커밋 금지(.gitignore 반영)- 컨테이너는 비루트 이미지(
nginxinc/nginx-unprivileged) 사용, 포트는 8080 고정
홈페이지(web)는 main 푸시 시 정적 파일을 VPS로 동기화합니다(컨테이너 미사용).
- 워크플로:
.github/workflows/deploy-web.yml - 트리거:
src/**변경 또는 수동 실행 - 동작: SSH/rsync로
src/→/srv/www/trr/동기화 (필요 시 nginx reload)
- Nginx vhost 설정 확인
www.trr.co.kr→root /srv/www/trr;(정적 서빙)blog.trr.co.kr→root /srv/traum_homepage/traum_blog/public;location /oauth/ { proxy_pass http://127.0.0.1:17203/; }
- 디렉터리 존재/권한
mkdir -p /srv/www/trr /srv/traum_homepage/traum_blog/public- 웹 서버 사용자(예:
www-data)가 읽을 수 있도록 퍼미션 확인
- (선택) nginx reload 권한 위임
- 워크플로는
sudo -n systemctl reload nginx || sudo -n nginx -s reload || true로 재시도를 시도합니다. - 비밀번호 없이 재로드를 허용하려면 호스트에서
sudoers항목을 제한적으로 추가하세요(사용자/경로는 환경에 맞게 조정).# /etc/sudoers.d/trr-nginx-reload deploy ALL=(root) NOPASSWD:/bin/systemctl reload nginx,/usr/sbin/nginx -s reloaddeploy를 배포 계정으로 치환- 보안상 필요한 커맨드만 허용
- 워크플로는
- 검증(예)
curl -sI https://www.trr.co.kr | sed -n '1,10p'curl -sI https://blog.trr.co.kr/admin/config.yml | rg -i '200|Content-Type: text/yaml'- 정적 자산 응답 헤더에
Cache-Control: public, max-age=2592000, immutable확인 - 스타일/스크립트 변경 시는 쿼리스트링 버전(
?v=YYYYMMDD)을 갱신
- 롤백 계획(요약)
- vhost를 이전 프록시 구성으로 되돌림:
www/blog를127.0.0.1:17201/17202로 프록시 - 필요 시
docker compose build web && docker compose up -d web
- vhost를 이전 프록시 구성으로 되돌림:
# 완전 차단
location = /oauth/metrics { return 403; }
# 또는 Basic Auth 적용 (예시)
# location = /oauth/metrics {
# auth_basic "Restricted";
# auth_basic_user_file /etc/nginx/.htpasswd_metrics;
# proxy_pass http://127.0.0.1:17203/metrics;
# }- 컨테이너 포트 바인딩은
.env로 조정 가능합니다(HTTP_BIND_HOST,HOMEPAGE_PORT).