init: Node starter (podman dev + Coolify deploy, Claude agent 골격)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal 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
17
.env.example
Normal 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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
npm-debug.log*
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
42
CLAUDE.md
Normal file
42
CLAUDE.md
Normal 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
22
Dockerfile
Normal 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
30
README.md
Normal 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
17
compose.yaml
Normal 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
21
package.json
Normal 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
11
scripts/check-db.js
Normal 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
10
scripts/check-minio.js
Normal 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
25
src/config.js
Normal 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
15
src/db.js
Normal 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
24
src/s3.js
Normal 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
33
src/server.js
Normal 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}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user