From e293f2cb87e58b2d256905866195c60a3cc932a3 Mon Sep 17 00:00:00 2001 From: 0310700 Date: Sun, 14 Jun 2026 16:20:37 +0900 Subject: [PATCH] =?UTF-8?q?init:=20Node=20starter=20(podman=20dev=20+=20Co?= =?UTF-8?q?olify=20deploy,=20Claude=20agent=20=EA=B3=A8=EA=B2=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .dockerignore | 10 ++++++++++ .env.example | 17 +++++++++++++++++ .gitignore | 7 +++++++ CLAUDE.md | 42 ++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 22 ++++++++++++++++++++++ README.md | 30 ++++++++++++++++++++++++++++++ compose.yaml | 17 +++++++++++++++++ package.json | 21 +++++++++++++++++++++ scripts/check-db.js | 11 +++++++++++ scripts/check-minio.js | 10 ++++++++++ src/config.js | 25 +++++++++++++++++++++++++ src/db.js | 15 +++++++++++++++ src/s3.js | 24 ++++++++++++++++++++++++ src/server.js | 33 +++++++++++++++++++++++++++++++++ 14 files changed, 284 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 compose.yaml create mode 100644 package.json create mode 100644 scripts/check-db.js create mode 100644 scripts/check-minio.js create mode 100644 src/config.js create mode 100644 src/db.js create mode 100644 src/s3.js create mode 100644 src/server.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4a53217 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +.env +.env.* +.git +.gitignore +Dockerfile +compose.yaml +README.md +CLAUDE.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..087ffa6 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# .env.example — 복사해서 .env 로 쓰세요: cp .env.example .env +# 주의: .env 는 git에 올리지 마세요(.gitignore에 포함됨). + +# --- DB --- +# 워크스페이스에 DATABASE_URL 이 이미 export 되어 있으면 비워둬도 됨(자동 사용). +# DATABASE_URL=postgresql://emp_<사번>:@coolify-db.aidev.svc.cluster.local:5432/appdb?search_path=emp_<사번> + +# --- MinIO / S3 --- +# endpoint 는 콘솔(minioc)이 아니라 API 호스트(minio)입니다. +S3_ENDPOINT=https://minio.bokdev.in +S3_REGION=us-east-1 +S3_BUCKET=coolify-user-data +S3_ACCESS_KEY_ID=REPLACE_WITH_YOUR_MINIO_ACCESS_KEY +S3_SECRET_ACCESS_KEY=REPLACE_WITH_YOUR_MINIO_SECRET_KEY + +# --- 앱 --- +PORT=3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43e5063 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.env +.env.* +!.env.example +npm-debug.log* +dist/ +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..181ad1e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md — 이 프로젝트에서 Claude Code가 따라야 할 규약 + +너는 사내 개발 워크스페이스에서 동작하는 코딩 에이전트다. 이 프로젝트는 **Node(ESM) 앱**이고, +**로컬 개발/테스트는 podman**, **배포는 Coolify(Gitea push → Dockerfile 자동빌드)** 로 한다. + +## 절대 규칙 +- **비밀값을 코드/커밋에 넣지 마라.** DB·MinIO 자격증명은 항상 환경변수(`.env`, 워크스페이스 env)에서만 읽는다. + `.env` 는 절대 커밋하지 않는다(`.gitignore`에 있음). 새 비밀 키가 필요하면 `.env.example` 에 placeholder만 추가. +- 컨테이너는 `docker` 명령으로 실행해도 되지만 **실제 런타임은 rootless podman**이다(`docker`는 podman 별칭). +- 운영 배포를 직접 하지 마라. 배포는 **git push → Coolify**가 담당한다. 너는 코드/Dockerfile만 책임진다. + +## 외부 연결 (이미 워크스페이스 env로 주입됨) +- **Postgres**: `DATABASE_URL` (직원 전용 schema로 격리, `search_path` 고정). 코드는 `src/db.js`를 통해 접근. +- **MinIO(S3)**: API endpoint `https://minio.bokdev.in` (콘솔 `minioc`가 아님), 버킷 `coolify-user-data`. + 키는 직원이 `.env`의 `S3_ACCESS_KEY_ID`/`S3_SECRET_ACCESS_KEY`에 채운다. 코드는 `src/s3.js`를 통해 접근. +- **LiteLLM**: AI 호출은 `~/.config/litellm/key` 의 virtual key를 쓰는 사내 gateway(`https://litellm.bok.or.kr`)를 경유. + +## 로컬 개발 워크플로 (podman) +```bash +npm install +cp .env.example .env # 그리고 MinIO 키 등 채우기 (DB는 자동주입이면 비워둬도 됨) +npm run dev # node --watch 로 핫리로드, http://localhost:3000 + +# 컨테이너로 동일하게 돌려보기 +podman build -t starter . +podman run --rm -p 3000:3000 --env-file .env \ + -e DATABASE_URL="$DATABASE_URL" starter +# 또는 +podman compose up --build +``` +연결 확인: `npm run db:check`, `npm run minio:check`, 또는 `/db` `/s3` 엔드포인트. + +## 배포 워크플로 (Coolify) +1. Gitea repo에 push (`main` 등 배포 브랜치). +2. Coolify가 이 **Dockerfile**로 자동 빌드/배포한다(로컬 podman 빌드와 동일 Dockerfile). +3. Coolify 쪽 Environment Variables 에 `DATABASE_URL`, `S3_*` 를 채운다(로컬 `.env`와 같은 키). +4. 포트는 컨테이너 `3000`. Coolify에서 도메인/포트 매핑. + +## 코드 규약 +- ESM(`import`/`export`), Node 22+. 외부 연결정보는 반드시 `src/config.js` 한 곳을 거친다. +- 새 외부 의존성을 추가하면 `package.json`에 반영하고 `npm install`로 lock 갱신. +- 변경 후에는 `npm run db:check` / `npm run minio:check`로 연결이 살아있는지 스스로 확인하라. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bf193dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# 로컬(podman)과 Coolify 자동빌드가 공유하는 단일 Dockerfile. +# - 로컬: podman build -t starter . && podman run --rm -p 3000:3000 --env-file .env starter +# - 배포: Gitea push → Coolify가 이 Dockerfile로 빌드/배포 +FROM node:22-alpine + +WORKDIR /app + +# 의존성 레이어 캐시 +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev || npm install --omit=dev + +# 앱 소스 +COPY . . + +ENV NODE_ENV=production \ + PORT=3000 +EXPOSE 3000 + +# 비루트 실행 +USER node + +CMD ["node", "src/server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..91dabe4 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# starter — Node 앱 스타터 (podman 개발 + Coolify 배포) + +사내 워크스페이스용 Node(ESM) 스타터. DB·MinIO 연결, podman 로컬개발, Coolify 자동배포가 미리 잡혀 있다. + +## 빠른 시작 +```bash +npm install +cp .env.example .env # MinIO 키 채우기 (DB는 워크스페이스가 자동주입) +npm run dev # http://localhost:3000 +``` +- `GET /healthz` — 헬스체크 +- `GET /db` — Postgres 연결 확인 (직원 전용 schema) +- `GET /s3` — MinIO 버킷 연결 확인 + +## 컨테이너로 실행 (podman) +```bash +podman build -t starter . +podman run --rm -p 3000:3000 --env-file .env -e DATABASE_URL="$DATABASE_URL" starter +# 또는: podman compose up --build +``` + +## 배포 (Coolify) +Gitea에 push → Coolify가 `Dockerfile`로 자동 빌드/배포. Coolify Env에 `DATABASE_URL`, `S3_*` 입력. + +## 연결정보 +- **DB**: 워크스페이스 env `DATABASE_URL` 자동주입 (본인 schema 격리) +- **MinIO(S3)**: endpoint `https://minio.bokdev.in`, bucket `coolify-user-data`, 키는 `.env`에 직접 입력 +- **AI**: `~/.config/litellm/key` 에 LiteLLM virtual key 입력 → Claude Code/Codex/Gemini 공용 + +자세한 작업 규약은 `CLAUDE.md` 참고. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..60b8c31 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +# 로컬 개발용 (podman compose 또는 podman play). 운영 배포는 Coolify가 담당. +# podman compose up --build +# DB/MinIO 접속정보는 워크스페이스 env + .env 에서 주입됨. +services: + app: + build: . + ports: + - "3000:3000" + env_file: + - .env + environment: + # 워크스페이스에 자동 주입된 값을 컨테이너로 전달 + DATABASE_URL: ${DATABASE_URL} + S3_ENDPOINT: ${S3_ENDPOINT:-https://minio.bokdev.in} + S3_BUCKET: ${S3_BUCKET:-coolify-user-data} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c09836e --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "starter", + "version": "0.1.0", + "description": "사내 Node 스타터 — podman 로컬개발 + Coolify(Dockerfile) 배포", + "type": "module", + "main": "src/server.js", + "scripts": { + "dev": "node --watch src/server.js", + "start": "node src/server.js", + "db:check": "node scripts/check-db.js", + "minio:check": "node scripts/check-minio.js" + }, + "engines": { + "node": ">=22" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "express": "^4.21.2", + "pg": "^8.13.1" + } +} diff --git a/scripts/check-db.js b/scripts/check-db.js new file mode 100644 index 0000000..1ad376d --- /dev/null +++ b/scripts/check-db.js @@ -0,0 +1,11 @@ +// DB 연결 빠른 확인: `npm run db:check` +import { pingDb, pool } from "../src/db.js"; + +try { + console.log("DB OK:", await pingDb()); +} catch (e) { + console.error("DB FAIL:", e.message); + process.exitCode = 1; +} finally { + await pool.end(); +} diff --git a/scripts/check-minio.js b/scripts/check-minio.js new file mode 100644 index 0000000..d809fb4 --- /dev/null +++ b/scripts/check-minio.js @@ -0,0 +1,10 @@ +// MinIO(S3) 연결 빠른 확인: `npm run minio:check` +// 먼저 ~/.config 또는 .env 에서 S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY 를 채워야 함. +import { pingS3 } from "../src/s3.js"; + +try { + console.log("S3 OK:", await pingS3()); +} catch (e) { + console.error("S3 FAIL:", e.message); + process.exitCode = 1; +} diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..e352b7c --- /dev/null +++ b/src/config.js @@ -0,0 +1,25 @@ +// 환경설정 — 모든 외부 연결정보는 환경변수에서만 읽는다(코드에 비밀값 금지). +// +// DB: 워크스페이스가 DATABASE_URL / PG* 를 자동 주입함(직원 전용 schema로 격리). +// MinIO(S3): 사내 오브젝트 스토리지. endpoint는 API 호스트(minio.bokdev.in), +// 콘솔(minioc.bokdev.in)이 아님에 주의. +// +// Coolify 배포 시에는 Coolify의 Environment Variables 화면에서 같은 키들을 채운다. + +export const config = { + port: parseInt(process.env.PORT || "3000", 10), + + // Postgres — 워크스페이스 자동주입. Coolify에선 본인 DB 접속정보를 넣을 것. + databaseUrl: process.env.DATABASE_URL || "", + + // MinIO / S3 호환 + s3: { + endpoint: process.env.S3_ENDPOINT || "https://minio.bokdev.in", + region: process.env.S3_REGION || "us-east-1", + bucket: process.env.S3_BUCKET || "coolify-user-data", + accessKeyId: process.env.S3_ACCESS_KEY_ID || "", + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "", + // MinIO는 path-style 필요 + forcePathStyle: true, + }, +}; diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..0c5aec4 --- /dev/null +++ b/src/db.js @@ -0,0 +1,15 @@ +// Postgres 연결 풀. DATABASE_URL 한 줄로 접속(직원 전용 schema가 search_path에 고정됨). +import pg from "pg"; +import { config } from "./config.js"; + +export const pool = new pg.Pool({ + connectionString: config.databaseUrl, + max: 5, +}); + +export async function pingDb() { + const { rows } = await pool.query( + "select current_user as user, current_schema as schema, now() as now" + ); + return rows[0]; +} diff --git a/src/s3.js b/src/s3.js new file mode 100644 index 0000000..6d99737 --- /dev/null +++ b/src/s3.js @@ -0,0 +1,24 @@ +// MinIO(S3 호환) 클라이언트. 자격증명은 env에서만 읽는다. +import { S3Client, ListObjectsV2Command, HeadBucketCommand } from "@aws-sdk/client-s3"; +import { config } from "./config.js"; + +export const s3 = new S3Client({ + endpoint: config.s3.endpoint, + region: config.s3.region, + forcePathStyle: config.s3.forcePathStyle, + credentials: { + accessKeyId: config.s3.accessKeyId, + secretAccessKey: config.s3.secretAccessKey, + }, +}); + +export async function pingS3() { + await s3.send(new HeadBucketCommand({ Bucket: config.s3.bucket })); + const out = await s3.send( + new ListObjectsV2Command({ Bucket: config.s3.bucket, MaxKeys: 5 }) + ); + return { + bucket: config.s3.bucket, + sampleKeys: (out.Contents || []).map((o) => o.Key), + }; +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..72b394d --- /dev/null +++ b/src/server.js @@ -0,0 +1,33 @@ +// 최소 Express 서버 — /healthz, /db, /s3 로 연결 확인. +import express from "express"; +import { config } from "./config.js"; +import { pingDb } from "./db.js"; +import { pingS3 } from "./s3.js"; + +const app = express(); + +app.get("/healthz", (_req, res) => res.json({ ok: true })); + +app.get("/db", async (_req, res) => { + try { + res.json({ ok: true, ...(await pingDb()) }); + } catch (e) { + res.status(500).json({ ok: false, error: String(e.message || e) }); + } +}); + +app.get("/s3", async (_req, res) => { + try { + res.json({ ok: true, ...(await pingS3()) }); + } catch (e) { + res.status(500).json({ ok: false, error: String(e.message || e) }); + } +}); + +app.get("/", (_req, res) => + res.json({ name: "starter", endpoints: ["/healthz", "/db", "/s3"] }) +); + +app.listen(config.port, "0.0.0.0", () => { + console.log(`starter listening on :${config.port}`); +});