init: Node starter (podman dev + Coolify deploy, Claude agent 골격)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0310700
2026-06-14 16:20:37 +09:00
commit e293f2cb87
14 changed files with 284 additions and 0 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
npm-debug.log
.env
.env.*
.git
.gitignore
Dockerfile
compose.yaml
README.md
CLAUDE.md

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# .env.example — 복사해서 .env 로 쓰세요: cp .env.example .env
# 주의: .env 는 git에 올리지 마세요(.gitignore에 포함됨).
# --- DB ---
# 워크스페이스에 DATABASE_URL 이 이미 export 되어 있으면 비워둬도 됨(자동 사용).
# DATABASE_URL=postgresql://emp_<사번>:<pw>@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

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
.env
.env.*
!.env.example
npm-debug.log*
dist/
.DS_Store

42
CLAUDE.md Normal file
View File

@@ -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`로 연결이 살아있는지 스스로 확인하라.

22
Dockerfile Normal file
View File

@@ -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"]

30
README.md Normal file
View File

@@ -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` 참고.

17
compose.yaml Normal file
View File

@@ -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}

21
package.json Normal file
View File

@@ -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"
}
}

11
scripts/check-db.js Normal file
View File

@@ -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();
}

10
scripts/check-minio.js Normal file
View File

@@ -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;
}

25
src/config.js Normal file
View File

@@ -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,
},
};

15
src/db.js Normal file
View File

@@ -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];
}

24
src/s3.js Normal file
View File

@@ -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),
};
}

33
src/server.js Normal file
View File

@@ -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}`);
});