diff --git a/.dockerignore b/.dockerignore
index 63a3791..40c18a7 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -24,7 +24,7 @@ Thumbs.db
*.log
npm-debug.log*
-# Environment files (use Railway/Vercel environment variables)
+# Environment files
.env
.env.*
!.env.example
diff --git a/.github/workflows/bot-deploy.yml b/.github/workflows/bot-deploy.yml
new file mode 100644
index 0000000..88e2b02
--- /dev/null
+++ b/.github/workflows/bot-deploy.yml
@@ -0,0 +1,102 @@
+name: π€ Bot Build & Deploy
+
+on:
+ push:
+ branches: [dev]
+ paths:
+ - 'packages/bot/**'
+ - 'packages/shared/**'
+ - 'packages/bot/Dockerfile'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: bot-deploy-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ ECR_REPOSITORY: study-admin-bot
+ AWS_REGION: ap-northeast-2
+
+jobs:
+ ci:
+ name: CI Gate
+ runs-on: ubuntu-latest
+ steps:
+ - name: β
μ½λ 체ν¬μμ
+ uses: actions/checkout@v4
+
+ - name: π¦ pnpm μ€μ
+ uses: pnpm/action-setup@v4
+
+ - name: π’ Node.js μ€μ
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'pnpm'
+
+ - name: π₯ μμ‘΄μ± μ€μΉ
+ run: pnpm install --frozen-lockfile
+
+ - name: π¨ shared λΉλ
+ run: pnpm --filter @blog-study/shared build
+
+ - name: π λ¦°νΈ
+ run: pnpm --filter @blog-study/bot lint
+
+ - name: π νμ
체ν¬
+ run: pnpm --filter @blog-study/bot typecheck
+
+ - name: π§ͺ ν
μ€νΈ
+ run: pnpm --filter @blog-study/bot test
+
+ build-and-push:
+ name: Build and Push to ECR
+ needs: ci
+ runs-on: ubuntu-latest
+ steps:
+ - name: β
μ½λ 체ν¬μμ
+ uses: actions/checkout@v4
+
+ - name: ποΈ Docker Buildx μ€μ
+ uses: docker/setup-buildx-action@v3
+
+ - name: π AWS μ격 μ¦λͺ
μ€μ
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ - name: π³ ECR λ‘κ·ΈμΈ
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v2
+
+ - name: π³ Docker μ΄λ―Έμ§ λΉλ λ° νΈμ (ARM64)
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./packages/bot/Dockerfile
+ push: true
+ platforms: linux/arm64
+ tags: |
+ ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
+ ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ deploy:
+ name: Deploy to EC2
+ needs: build-and-push
+ runs-on: ubuntu-latest
+ steps:
+ - name: π SSHλ‘ λ°°ν¬ μ€ν¬λ¦½νΈ μ€ν
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.SSH_HOST }}
+ username: ${{ secrets.SSH_USER }}
+ key: ${{ secrets.SSH_PRIVATE_KEY }}
+ script: |
+ ~/study-admin-bot/deploy.sh ${{ github.sha }}
diff --git a/CLAUDE.md b/CLAUDE.md
index 589019b..355ac77 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,9 +6,11 @@
```
packages/
-βββ bot/ # Discord λ΄ (μ€μΌμ€λ¬ + μ΄λ²€νΈ νΈλ€λ¬λ§, μ¬λμ 컀맨λ μμ) β AWS EC2 λ°°ν¬
+βββ bot/ # Discord λ΄ (μ€μΌμ€λ¬ + μ΄λ²€νΈ νΈλ€λ¬λ§, μ¬λμ 컀맨λ μμ) β AWS EC2 (Docker)
βββ web/ # Next.js 16 λμ보λ β Vercel λ°°ν¬
βββ shared/ # 곡μ μ½λ (DB μ€ν€λ§, νμ
, μ νΈ)
+deploy/
+βββ bot/ # EC2 λ°°ν¬ μ€ν¬λ¦½νΈ (deploy.sh) β 리ν¬μ 컀λ°νμ§ μμ, EC2μ μ§μ λ°°μΉ
```
**λͺ¨λ
Έλ ν¬**: pnpm workspace (`pnpm-workspace.yaml`)
@@ -22,7 +24,8 @@ packages/
| Web | Next.js 16 App Router, React 19, shadcn/ui, Tailwind CSS v4, Tiptap (λ¦¬μΉ μλν°), sonner (ν μ€νΈ), Framer Motion (λλ© μ λλ©μ΄μ
) |
| DB | Supabase PostgreSQL + Drizzle ORM (Transaction Pooler, `prepare: false`) |
| Auth | Supabase Auth (Discord OAuth) + `@supabase/ssr` |
-| λ°°ν¬ | AWS EC2 (bot), Vercel (web), Supabase (DB + Auth) |
+| λ°°ν¬ | AWS EC2 Docker (bot), Vercel (web), Supabase (DB + Auth) |
+| CI/CD | GitHub Actions β ECR β SSH deploy (bot), Vercel Git Integration (web) |
## κ°λ° λͺ
λ Ήμ΄
@@ -102,6 +105,9 @@ pnpm --filter @blog-study/bot rss-collect # μλ RSS μμ§ (λ΄ μμ΄)
| `packages/web/src/components/landing/landing-client.tsx` | λλ© νμ΄μ§ ν΄λΌμ΄μΈνΈ (7μΉμ
: Hero, Stats, Bento, HowItWorks, Marquee, CTA, Footer) |
| `packages/web/src/components/landing/motion.tsx` | λλ© μ λλ©μ΄μ
μ»΄ν¬λνΈ (FadeUp, StaggerContainer, CountUp, DrawLine) |
| `packages/web/public/logo.svg` | ν λ‘κ³ SVG (ν½ν κ·Έλ¨ + ν
μ€νΈ) |
+| `packages/bot/Dockerfile` | λ΄ Docker μ΄λ―Έμ§ (multi-stage, node:22-alpine) |
+| `.github/workflows/bot-deploy.yml` | λ΄ CI/CD (CI Gate β ECR λΉλ/νΈμ β SSH λ°°ν¬) |
+| `.github/workflows/ci.yml` | PR/push CI (lint, typecheck, test, build) |
## μΈμ¦ ꡬ쑰
@@ -219,6 +225,15 @@ npx drizzle-kit push --force
| `docs/plans/26-03-08-landing-page-redesign-design.md` | λλ© νμ΄μ§ 리λμμΈ λμμΈ λ¬Έμ |
| `docs/plans/26-03-08-landing-page-redesign.md` | λλ© νμ΄μ§ ꡬν νλ |
+## λ΄ λ°°ν¬ (CI/CD)
+
+- **νμ΄νλΌμΈ**: `dev` push β CI Gate (lint+typecheck+test) β ECR λΉλ(ARM64) β SSH λ°°ν¬
+- **νΈλ¦¬κ±°**: `packages/bot/**`, `packages/shared/**` λ³κ²½ μ + `workflow_dispatch`
+- **EC2**: illdan-mgmt (t4g ARM64), `~/study-admin-bot/deploy.sh` + `.env`
+- **ECR**: `101548339709.dkr.ecr.ap-northeast-2.amazonaws.com/study-admin-bot`
+- **deploy.sh**: ECR λ‘κ·ΈμΈ β pull β 컨ν
μ΄λ κ΅μ²΄ β health check β Discord μΉν
μλ¦Ό
+- **μ£Όμ**: `deploy/bot/deploy.sh`λ 컀λ°νμ§ μμ (EC2μ μ§μ λ°°μΉ)
+
## docs νμΌλͺ
컨벀μ
`yy-mm-dd-{μ€λͺ
}.md` β μ: `26-03-03-system-architecture.md`
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index d747ac4..e3f31e1 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -31,7 +31,7 @@ graph TB
end
end
- subgraph Bot["Discord Bot Β· AWS EC2"]
+ subgraph Bot["Discord Bot Β· AWS EC2 (Docker)"]
SCH["Schedulers
pg-boss"]
EVT["Event Handlers
discord.js v14"]
SVC["Service Layer
RSS Β· Fine Β· Curation Β· Score"]
@@ -126,7 +126,7 @@ graph LR
| ν¨ν€μ§ | μ€λͺ
| λ°°ν¬ |
|--------|------|------|
| `packages/shared` | Drizzle μ€ν€λ§, νμ
, μ νΈ | npm (workspace λ΄λΆ) |
-| `packages/bot` | Discord λ΄ (μ€μΌμ€λ¬ + μ΄λ²€νΈ νΈλ€λ¬, μ¬λμ 컀맨λ μμ) | AWS EC2 |
+| `packages/bot` | Discord λ΄ (μ€μΌμ€λ¬ + μ΄λ²€νΈ νΈλ€λ¬, μ¬λμ 컀맨λ μμ) | AWS EC2 (Docker) |
| `packages/web` | Next.js λμ보λ, API Routes | Vercel |
## μΈμ¦ μν€ν
μ²
@@ -435,12 +435,22 @@ erDiagram
```mermaid
graph LR
+ subgraph GHA["GitHub Actions"]
+ CI["CI Gate
(lint+typecheck+test)"]
+ ECR_PUSH["Docker Build
β ECR Push (ARM64)"]
+ SSH["SSH Deploy"]
+ end
+
subgraph Vercel["Vercel (ICN)"]
WEB_DEPLOY["@blog-study/web
Next.js"]
end
- subgraph EC2["AWS EC2"]
- BOT_DEPLOY["@blog-study/bot
discord.js + pm2"]
+ subgraph EC2["AWS EC2 (t4g)"]
+ BOT_DEPLOY["@blog-study/bot
Docker Container"]
+ end
+
+ subgraph AWS["AWS"]
+ ECR["ECR
study-admin-bot"]
end
subgraph Supabase["Supabase (ap-northeast-2)"]
@@ -448,6 +458,11 @@ graph LR
SA["Supabase Auth"]
end
+ CI --> ECR_PUSH
+ ECR_PUSH --> ECR
+ ECR_PUSH --> SSH
+ SSH --> BOT_DEPLOY
+ BOT_DEPLOY -->|pull| ECR
WEB_DEPLOY --> PG
WEB_DEPLOY --> SA
BOT_DEPLOY --> PG
diff --git a/packages/bot/.dockerignore b/packages/bot/.dockerignore
deleted file mode 100644
index 392f954..0000000
--- a/packages/bot/.dockerignore
+++ /dev/null
@@ -1,36 +0,0 @@
-# Dependencies
-node_modules
-
-# Build output (will be built in container)
-dist
-
-# Development files
-*.test.ts
-*.spec.ts
-vitest.config.ts
-tsconfig.tsbuildinfo
-
-# IDE
-.vscode
-.idea
-
-# OS
-.DS_Store
-Thumbs.db
-
-# Logs
-*.log
-npm-debug.log*
-
-# Environment files (use Railway environment variables)
-.env
-.env.*
-!.env.example
-
-# Git
-.git
-.gitignore
-
-# Documentation
-*.md
-!README.md
diff --git a/packages/bot/Dockerfile b/packages/bot/Dockerfile
index b591372..6a0268b 100644
--- a/packages/bot/Dockerfile
+++ b/packages/bot/Dockerfile
@@ -1,7 +1,6 @@
# Build stage
-FROM node:20-alpine AS builder
+FROM node:22-alpine AS builder
-# Install pnpm
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
@@ -11,7 +10,6 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY packages/shared/package.json ./packages/shared/
COPY packages/bot/package.json ./packages/bot/
-# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
@@ -19,32 +17,26 @@ COPY tsconfig.json ./
COPY packages/shared ./packages/shared
COPY packages/bot ./packages/bot
-# Build shared package first, then bot
-RUN pnpm --filter @blog-study/shared build
-RUN pnpm --filter @blog-study/bot build
+# Build shared β bot
+RUN pnpm --filter @blog-study/shared build && \
+ pnpm --filter @blog-study/bot build
# Production stage
-FROM node:20-alpine AS runner
+FROM node:22-alpine AS runner
-# Install pnpm
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
-# Copy workspace configuration
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY packages/shared/package.json ./packages/shared/
COPY packages/bot/package.json ./packages/bot/
-# Install production dependencies only
RUN pnpm install --frozen-lockfile --prod
-# Copy built files
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
COPY --from=builder /app/packages/bot/dist ./packages/bot/dist
-# Set environment
ENV NODE_ENV=production
-# Run the bot
CMD ["node", "packages/bot/dist/index.js"]
diff --git a/packages/bot/railway.json b/packages/bot/railway.json
deleted file mode 100644
index d8af1cd..0000000
--- a/packages/bot/railway.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "$schema": "https://railway.app/railway.schema.json",
- "build": {
- "builder": "DOCKERFILE",
- "dockerfilePath": "packages/bot/Dockerfile",
- "watchPatterns": [
- "packages/bot/**",
- "packages/shared/**"
- ]
- },
- "deploy": {
- "startCommand": "node packages/bot/dist/index.js",
- "healthcheckPath": null,
- "healthcheckTimeout": 300,
- "restartPolicyType": "ON_FAILURE",
- "restartPolicyMaxRetries": 10
- }
-}