-
Notifications
You must be signed in to change notification settings - Fork 1
v2026.0616.1535 #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
v2026.0616.1535 #189
Changes from all commits
af8eb4f
55a41da
21ef328
7c031b3
690ade9
5db4eb6
60db532
8995bc2
f5dee77
ca19daf
472940b
a6d7716
8a544ba
1fc1815
90a27f8
4d2d05c
c71898c
c7ec4a0
523611a
5dc73d3
a95b6d6
39d5a88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| node_modules | ||
| .next | ||
| .git | ||
| .github | ||
| .vercel | ||
| .env | ||
| .env.* | ||
| npm-debug.log* | ||
| Dockerfile | ||
| .dockerignore | ||
| compose.yaml | ||
| docker-compose.yml | ||
| README.md | ||
| .claude | ||
| .agents | ||
| coverage | ||
| *.log |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| # ───────────────────────────────────────────────────────────────── | ||
| # Deploy — Flooding-Client-V2 | ||
| # develop → gsmsv-preview 환경 / main → gsmsv-production 환경 | ||
| # | ||
| # 필요한 GitHub Environments: `gsmsv-preview`, `gsmsv-production` | ||
| # (Vercel 연동이 만든 `Preview`/`Production` 과 이름을 구분한다) | ||
| # - Variables(공개값): NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_DG_CLIENT_ID, | ||
| # NEXT_PUBLIC_DG_REDIRECT_URL, NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_SENTRY_DSN, | ||
| # IMAGE_TAG(develop|main), SERVER_NAME, COOKIE_SECURE | ||
| # - Secrets: SENTRY_AUTH_TOKEN, YOUTUBE_API_KEY, | ||
| # VM_HOST, VM_PORT, VM_USER, VM_SSH_KEY | ||
| # (서버 SENTRY_DSN 은 빌드에 박힌 NEXT_PUBLIC_SENTRY_DSN 으로 폴백 → 불필요) | ||
| # (GHCR pull 은 배포 시점 GITHUB_TOKEN 으로 처리 → 별도 토큰 불필요) | ||
| # | ||
| # VM 일회성 준비: deploy 공개키를 VM authorized_keys 에 등록, docker 설치 | ||
| # ───────────────────────────────────────────────────────────────── | ||
| name: Deploy | ||
|
|
||
| on: | ||
| push: | ||
| branches: [develop, main] | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| env: | ||
| IMAGE: ghcr.io/team-incube/flooding-client | ||
|
|
||
| jobs: | ||
| build: | ||
| name: Build & Push (GHCR) | ||
| runs-on: ubuntu-latest | ||
| environment: ${{ github.ref_name == 'main' && 'gsmsv-production' || 'gsmsv-preview' }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Log in to GHCR (GITHUB_TOKEN, no PAT) | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Build & push (amd64) | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: . | ||
| push: true | ||
| platforms: linux/amd64 | ||
| tags: | | ||
| ${{ env.IMAGE }}:${{ github.ref_name }} | ||
| ${{ env.IMAGE }}:${{ github.ref_name }}-${{ github.sha }} | ||
| build-args: | | ||
| NEXT_PUBLIC_BASE_URL=${{ vars.NEXT_PUBLIC_BASE_URL }} | ||
| NEXT_PUBLIC_DG_CLIENT_ID=${{ vars.NEXT_PUBLIC_DG_CLIENT_ID }} | ||
| NEXT_PUBLIC_DG_REDIRECT_URL=${{ vars.NEXT_PUBLIC_DG_REDIRECT_URL }} | ||
| NEXT_PUBLIC_SITE_URL=${{ vars.NEXT_PUBLIC_SITE_URL }} | ||
| NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} | ||
| SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} | ||
|
|
||
| deploy: | ||
| name: Deploy to VM | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| environment: ${{ github.ref_name == 'main' && 'gsmsv-production' || 'gsmsv-preview' }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| # compose.yaml + nginx/ 를 VM 으로 동기화 (레포가 source of truth) | ||
| - name: Sync deploy files to VM | ||
| uses: appleboy/scp-action@v0.1.7 | ||
| with: | ||
| host: ${{ secrets.VM_HOST }} | ||
| port: ${{ secrets.VM_PORT }} | ||
| username: ${{ secrets.VM_USER }} | ||
| key: ${{ secrets.VM_SSH_KEY }} | ||
| source: "compose.yaml,nginx/" | ||
| target: "flooding" | ||
| overwrite: true | ||
|
|
||
| - name: Pull image & restart | ||
| uses: appleboy/ssh-action@v1 | ||
| with: | ||
| host: ${{ secrets.VM_HOST }} | ||
| port: ${{ secrets.VM_PORT }} | ||
| username: ${{ secrets.VM_USER }} | ||
| key: ${{ secrets.VM_SSH_KEY }} | ||
| script: | | ||
| set -e | ||
| cd ~/flooding | ||
| # 런타임 .env 를 환경 변수/시크릿에서 생성 (VM 에 수기 보관 안 함) | ||
| cat > .env <<EOF | ||
| IMAGE_TAG=${{ vars.IMAGE_TAG }} | ||
| SERVER_NAME=${{ vars.SERVER_NAME }} | ||
| COOKIE_SECURE=${{ vars.COOKIE_SECURE }} | ||
| YOUTUBE_API_KEY=${{ secrets.YOUTUBE_API_KEY }} | ||
| EOF | ||
| # GHCR 로그인: 배포 시점에만 유효한 GITHUB_TOKEN 사용 (별도 PAT 불필요) | ||
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | ||
| docker compose pull | ||
| docker compose up -d | ||
| docker logout ghcr.io || true | ||
| docker image prune -f |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # syntax=docker/dockerfile:1 | ||
|
|
||
| # ---- deps: install dependencies ---- | ||
| FROM node:22-slim AS deps | ||
| WORKDIR /app | ||
| COPY package.json package-lock.json ./ | ||
| RUN npm ci | ||
|
|
||
| # ---- builder: build Next.js (standalone) ---- | ||
| FROM node:22-slim AS builder | ||
| WORKDIR /app | ||
| COPY --from=deps /app/node_modules ./node_modules | ||
| COPY . . | ||
|
|
||
| # NEXT_PUBLIC_* are inlined at build time, so they must be provided here. | ||
| ARG NEXT_PUBLIC_BASE_URL | ||
| ARG NEXT_PUBLIC_DG_CLIENT_ID | ||
| ARG NEXT_PUBLIC_DG_REDIRECT_URL | ||
| ARG NEXT_PUBLIC_SITE_URL | ||
| ARG NEXT_PUBLIC_SENTRY_DSN | ||
| # Build-time only: Sentry source map upload. | ||
| ARG SENTRY_AUTH_TOKEN | ||
|
|
||
| ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL \ | ||
| NEXT_PUBLIC_DG_CLIENT_ID=$NEXT_PUBLIC_DG_CLIENT_ID \ | ||
| NEXT_PUBLIC_DG_REDIRECT_URL=$NEXT_PUBLIC_DG_REDIRECT_URL \ | ||
| NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL \ | ||
| NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \ | ||
| SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN \ | ||
| NEXT_TELEMETRY_DISABLED=1 | ||
|
|
||
| RUN npm run build | ||
|
|
||
| # ---- runner: minimal runtime image ---- | ||
| FROM node:22-slim AS runner | ||
| WORKDIR /app | ||
| ENV NODE_ENV=production \ | ||
| NEXT_TELEMETRY_DISABLED=1 \ | ||
| PORT=3000 \ | ||
| HOSTNAME=0.0.0.0 | ||
|
|
||
| RUN groupadd -r nodejs && useradd -r -g nodejs nextjs | ||
|
|
||
| # public assets + standalone server + static chunks | ||
| COPY --from=builder --chown=nextjs:nodejs /app/public ./public | ||
| COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | ||
| COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | ||
|
|
||
| USER nextjs | ||
| EXPOSE 3000 | ||
|
|
||
| # standalone output entrypoint (NOT `next start`) | ||
| CMD ["node", "server.js"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # GSM SV VM 배포 구성 (app + nginx 리버스 프록시). | ||
| # - dev: HTTP만 사용 (nginx 80 → app:3000). | ||
| # - prod: certbot 프로파일로 인증서 발급 후 nginx/templates 를 TLS 템플릿으로 교체하고 443 활성화. | ||
| # 환경값/시크릿은 같은 디렉터리의 .env 에서 주입 (.env.example 참고). | ||
| services: | ||
| app: | ||
| image: ghcr.io/team-incube/flooding-client:${IMAGE_TAG:-develop} | ||
| pull_policy: always | ||
| env_file: | ||
| - .env | ||
| expose: | ||
| - "3000" # nginx 가 프론트, 외부로 직접 공개하지 않음 | ||
| restart: unless-stopped | ||
|
|
||
| nginx: | ||
| image: nginx:1.27-alpine | ||
| depends_on: | ||
| - app | ||
| ports: | ||
| - "80:80" | ||
| # 프로덕션 TLS 시 주석 해제 | ||
| # - "443:443" | ||
| environment: | ||
| # nginx 공식 이미지의 envsubst 는 "환경에 정의된 변수"만 치환하므로 | ||
| # SERVER_NAME 만 정의 → 템플릿의 $host 등 nginx 변수는 그대로 보존됨. | ||
| - SERVER_NAME=${SERVER_NAME:-_} | ||
| volumes: | ||
| - ./nginx/templates:/etc/nginx/templates:ro | ||
| - acme-challenge:/var/www/certbot:ro | ||
| - letsencrypt:/etc/letsencrypt:ro | ||
| restart: unless-stopped | ||
|
|
||
| # 프로덕션 전용. 평소엔 안 뜬다. 사용: docker compose --profile prod up -d | ||
| # 최초 발급: docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d <DOMAIN> | ||
| certbot: | ||
| image: certbot/certbot | ||
| profiles: | ||
| - prod | ||
| volumes: | ||
| - acme-challenge:/var/www/certbot | ||
| - letsencrypt:/etc/letsencrypt | ||
| # 인증서 자동 갱신 루프 | ||
| entrypoint: /bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
가장 최근에 백그라운드로 실행된 프로세스(여기서는 entrypoint: /bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $$!; done;' |
||
|
|
||
| volumes: | ||
| acme-challenge: | ||
| letsencrypt: | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # HTTP 리버스 프록시 (dev/검증). envsubst 가 ${SERVER_NAME} 만 치환한다. | ||
| server { | ||
| listen 80; | ||
| server_name ${SERVER_NAME}; | ||
|
|
||
| # 이미지 업로드(아바타·클럽 썸네일) 대비. nginx 기본 1m 초과 거부 방지. | ||
| client_max_body_size 10m; | ||
|
|
||
| # 공통 프록시 헤더 (server 레벨 → 하위 location 이 상속) | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; | ||
| proxy_set_header X-Real-IP $remote_addr; | ||
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| proxy_set_header X-Forwarded-Proto $scheme; | ||
| proxy_set_header Connection ""; | ||
|
|
||
| # Let's Encrypt HTTP-01 챌린지 경로 (TLS 준비용, dev 에선 빈 볼륨이라 무해) | ||
| location /.well-known/acme-challenge/ { | ||
| root /var/www/certbot; | ||
| } | ||
|
|
||
| # SSE 전용: 버퍼링 끄고 장시간 연결 유지. | ||
| # (앱도 X-Accel-Buffering: no 를 보내지만, 장시간 read/send timeout 은 여기서만 지정 가능) | ||
| location = /api/dormitory/music/subscribe { | ||
| proxy_pass http://app:3000; | ||
| proxy_buffering off; | ||
| proxy_cache off; | ||
| proxy_read_timeout 3600s; | ||
| proxy_send_timeout 3600s; | ||
| } | ||
| location = /api/dormitory/studies/attendance { | ||
| proxy_pass http://app:3000; | ||
| proxy_buffering off; | ||
| proxy_cache off; | ||
| proxy_read_timeout 3600s; | ||
| proxy_send_timeout 3600s; | ||
| } | ||
|
|
||
| # 일반 요청: 기본 버퍼링 유지 | ||
| location / { | ||
| proxy_pass http://app:3000; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # 프로덕션 TLS 참고용 템플릿. | ||
| # 활성화 절차: | ||
| # 1) compose 의 nginx 443 포트와 certbot(prod 프로파일)을 켠다. | ||
| # 2) docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d <DOMAIN> | ||
| # 3) 이 파일을 nginx/templates/default.conf.template 로 교체(기존 HTTP 템플릿 대체)하고 | ||
| # <DOMAIN> 을 실제 도메인(flooding.kr)으로 치환. | ||
| # 4) docker compose up -d --force-recreate nginx | ||
| # envsubst 는 ${SERVER_NAME} 만 치환한다. | ||
|
|
||
| server { | ||
| listen 80; | ||
| server_name ${SERVER_NAME}; | ||
|
|
||
| location /.well-known/acme-challenge/ { | ||
| root /var/www/certbot; | ||
| } | ||
| location / { | ||
| return 301 https://$host$request_uri; | ||
| } | ||
| } | ||
|
|
||
| server { | ||
| listen 443 ssl; | ||
| http2 on; | ||
| server_name ${SERVER_NAME}; | ||
|
|
||
| ssl_certificate /etc/letsencrypt/live/<DOMAIN>/fullchain.pem; | ||
| ssl_certificate_key /etc/letsencrypt/live/<DOMAIN>/privkey.pem; | ||
|
|
||
| client_max_body_size 10m; | ||
|
|
||
| # 공통 프록시 헤더 (server 레벨 → 하위 location 이 상속) | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; | ||
| proxy_set_header X-Real-IP $remote_addr; | ||
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| proxy_set_header X-Forwarded-Proto $scheme; | ||
| proxy_set_header Connection ""; | ||
|
|
||
| # SSE 전용: 버퍼링 끄고 장시간 연결 유지 | ||
| location = /api/dormitory/music/subscribe { | ||
| proxy_pass http://app:3000; | ||
| proxy_buffering off; | ||
| proxy_cache off; | ||
| proxy_read_timeout 3600s; | ||
| proxy_send_timeout 3600s; | ||
| } | ||
| location = /api/dormitory/studies/attendance { | ||
| proxy_pass http://app:3000; | ||
| proxy_buffering off; | ||
| proxy_cache off; | ||
| proxy_read_timeout 3600s; | ||
| proxy_send_timeout 3600s; | ||
| } | ||
|
|
||
| # 일반 요청: 기본 버퍼링 유지 | ||
| location / { | ||
| proxy_pass http://app:3000; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SENTRY_AUTH_TOKEN과 같은 민감한 자격 증명을ARG및ENV를 통해 빌드 타임에 전달하면, 해당 값이 빌드 중간 레이어 및 이미지 히스토리에 고스란히 노출되어 보안상 취약할 수 있습니다 (예:docker history명령어로 조회 가능).이러한 보안 위험을 방지하기 위해 Docker BuildKit의 비밀 값 마운트 기능(
--mount=type=secret)을 사용하거나, 최소한 최종 이미지에 노출되지 않도록ENV설정을 제거하고 빌드 명령어 실행 시점에만 주입하는 방식을 권장합니다.BuildKit 비밀 값 마운트 예시:
RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \ SENTRY_AUTH_TOKEN=$(cat /run/secrets/SENTRY_AUTH_TOKEN) npm run build