feat(deploy): 도커 배포(deploy/docker: compose+Dockerfile+deploy.sh) — 우리 환경용. 렌더러가 FabriX/OpenRouter 템플릿 자동 선택, 호스트 변수화. bare deploy.sh 는 git pull 포함. Phoenix 도커 compose
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f55e673cb5
commit
ea1fd350da
@@ -13,3 +13,5 @@ deploy/langfuse/docker-compose.override.yml
|
||||
|
||||
# render_opencode.py 가 환경마다 만드는 파일 — 서버마다 다름
|
||||
opencode/opencode.json
|
||||
deploy/docker/.env
|
||||
deploy/phoenix/.env
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export PYTHONUTF8=1
|
||||
# git 안이면 최신으로(고객사: /www/abap-ito). 로컬 수정이 있어 못 당기면 멈춤 — 렌더 산출물은 git 밖이라 안 걸림
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then git -C "$(git rev-parse --show-toplevel)" pull --ff-only; fi
|
||||
# 필수 키 비어 있으면 여기서 멈춤 — 비어 있는 채로 띄우면 채팅 때 "누락" 에러가 나서 원인 찾기 어려움
|
||||
for k in AAF_FABRIX_BASE_URL AAF_FABRIX_CLIENT_KEY AAF_FABRIX_OPENAPI_TOKEN DB_HOST; do
|
||||
grep -qE "^$k=.+" .env || { echo "!! .env 에 $k 가 비어 있음 — 채우고 다시"; exit 1; }
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# CodeAssist 도커 배포 .env — 이 폴더에 .env 로 복사해서 채움. be 컨테이너 환경변수로 그대로 들어감.
|
||||
|
||||
# 바깥 포트 (앱이 http://<서버>:<BE_PORT>/api/v1 로 붙음)
|
||||
BE_PORT=8080
|
||||
|
||||
# LLM — 우리 환경은 OpenRouter 직접(고객사 FabriX 키 AAF_* 는 비움 → 렌더러가 OpenRouter 템플릿을 고름)
|
||||
OPENROUTER_API_KEY=
|
||||
OPENCODE_VISION_MODEL=openrouter/google/gemma-4-31b-it
|
||||
|
||||
# 토큰 서명 키 (비우면 data/.auth_secret 자동 생성)
|
||||
AUTH_SECRET=
|
||||
|
||||
# DB — 비우면 SQLite(be_data 볼륨). 팀 PostgreSQL 쓰려면 채움
|
||||
DB_HOST=
|
||||
DB_PORT=5432
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
||||
DB_SCHEMA=codeassist
|
||||
DB_SSLMODE=prefer
|
||||
|
||||
# 앱(Tauri)에서 오는 요청 허용 origin
|
||||
CORS_ORIGINS=http://tauri.localhost,tauri://localhost,http://localhost:15173
|
||||
|
||||
# 채팅 타임아웃(초)
|
||||
STREAM_FIRST_EVENT_TIMEOUT_S=300
|
||||
STREAM_TURN_TIMEOUT_S=600
|
||||
|
||||
# SAP 조회 MCP — 같은 서버의 abap-opencode compose 에 mcp 컨테이너가 있으면 그 주소. 비우면 끔
|
||||
MCP_HOST=mcp
|
||||
MCP_PORT=3200
|
||||
MCP_API_KEY=
|
||||
|
||||
# 관측(Phoenix) — deploy/phoenix 로 띄운 주소. 비우면 안 보냄
|
||||
PHOENIX_HOST=http://20.200.170.37:6006
|
||||
TRACE_PROJECT=codeassist
|
||||
PHOENIX_UI_PASSWORD=
|
||||
@@ -0,0 +1,12 @@
|
||||
# CodeAssist 백엔드 — Django(ASGI/uvicorn). 기동 때 migrate → OpenCode 설정 렌더 → 서버.
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
ENV PYTHONUTF8=1 PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=config.settings
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY manage.py ./
|
||||
COPY config ./config
|
||||
COPY apps ./apps
|
||||
COPY common ./common
|
||||
# opencode/ 는 compose 가 bind 로 끼움(/app/opencode) — 렌더 결과 opencode.json 을 opencode 컨테이너도 같은 폴더로 봄
|
||||
CMD ["sh", "-c", "python manage.py migrate --noinput && ([ -f seed/snippets.db ] && python manage.py import_snippets seed/snippets.db || true) && python opencode/render_opencode.py && uvicorn config.asgi:application --host 0.0.0.0 --port 8080"]
|
||||
@@ -0,0 +1,9 @@
|
||||
# OpenCode 헤드리스 서버 — 고객사 opencode-bin 과 같은 1.18.6 으로 고정
|
||||
FROM node:22-bookworm-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git ripgrep \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install -g --no-audit --no-fund opencode-ai@1.18.6
|
||||
WORKDIR /workspace
|
||||
EXPOSE 4096
|
||||
# .opencode/tools 의 커스텀 도구가 @opencode-ai/plugin 을 씀 — bind 된 workspace 라 기동 때 설치(있으면 건너뜀)
|
||||
CMD ["sh", "-c", "cd /workspace/.opencode && npm install --no-audit --no-fund --silent >/dev/null 2>&1; cd /workspace && opencode serve --hostname 0.0.0.0 --port 4096"]
|
||||
@@ -0,0 +1,35 @@
|
||||
# CodeAssist 백엔드 — 도커 배포 (우리 환경용)
|
||||
|
||||
고객사(도커 없음)는 `5_django_backend/deploy.sh`(bare) 를 씀. 여긴 도커 되는 서버(20.200.170.37 등)용.
|
||||
둘 다 같은 저장소 구조를 쓰고, 배포 스크립트만 다름:
|
||||
|
||||
| 환경 | 스크립트 | 하는 일 |
|
||||
|---|---|---|
|
||||
| 우리(도커) | `deploy/docker/deploy.sh [up\|update\|stop\|logs\|status]` | (update 면 git pull) → 이미지 빌드 → be·opencode 기동 → 헬스 확인 |
|
||||
| 고객사(bare) | `deploy.sh` | git pull → .env 키 보정 → migrate → 렌더 → opencode-bin·uvicorn 재기동 → 헬스 |
|
||||
|
||||
## 처음 한 번
|
||||
|
||||
```bash
|
||||
cd <저장소>/code-assistant/5_django_backend/deploy/docker # 고객사 repo 구조 기준. 우리 git 이면 5_django_backend/deploy/docker
|
||||
cp .env.example .env && vi .env # OPENROUTER_API_KEY, PHOENIX_HOST 정도만 채우면 됨
|
||||
./deploy.sh
|
||||
```
|
||||
컨테이너 2개: `be`(Django, 바깥 `BE_PORT`=8080) + `opencode`(OpenCode 1.18.6). 저장소의 `opencode/` 폴더를 둘 다 bind 로 봐서
|
||||
AGENTS.md·위키·커스텀 도구가 그대로 먹고, be 가 렌더한 `opencode.json` 을 opencode 가 바로 읽음.
|
||||
|
||||
## 이후
|
||||
|
||||
```bash
|
||||
./deploy.sh update # 최신 코드로 갱신
|
||||
./deploy.sh status # 상태 + 헬스
|
||||
./deploy.sh logs
|
||||
```
|
||||
|
||||
## 알아둘 것
|
||||
|
||||
- LLM: `.env` 에 `AAF_FABRIX_*` 가 없으면 렌더러가 OpenRouter 템플릿(`opencode/opencode.openrouter.json.tmpl`)을 고름. 고객사는 FabriX 템플릿.
|
||||
- DB: `DB_HOST` 비우면 SQLite(`be_data` 볼륨). 팀 PostgreSQL 쓰려면 DB_* 채움(스키마 `codeassist`).
|
||||
- SAP 조회(MCP): 같은 서버에 abap-opencode compose 가 떠 있으면 그 `mcp` 컨테이너를 쓸 수 있음 — 두 compose 를 같은 네트워크로 묶어야 함(아직 안 함). 비우면 꺼짐.
|
||||
- 관측: `PHOENIX_HOST=http://<서버>:6006` (`deploy/phoenix/docker-compose.yml` 로 띄운 것). 프로젝트 `codeassist`.
|
||||
- 앱(Tauri) 쪽은 `2_frontend/.env.production` 의 `VITE_API_BASE_URL` 을 이 서버로 바꿔 빌드해야 붙음.
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# CodeAssist 도커 배포 — 우리 환경용. 이거 하나면 됨. (ABAP_OPENCODE web/deploy/deploy.sh 와 같은 명령 모양)
|
||||
#
|
||||
# ./deploy.sh 빌드 + 기동 (떠 있으면 갱신)
|
||||
# ./deploy.sh update git pull 까지 하고 갱신
|
||||
# ./deploy.sh stop 종료 (데이터 볼륨은 남음)
|
||||
# ./deploy.sh logs 로그
|
||||
# ./deploy.sh status 상태
|
||||
#
|
||||
# 전제: 이 폴더에 .env (cp .env.example .env 후 채움)
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$HERE"
|
||||
CMD="${1:-up}"
|
||||
|
||||
command -v docker >/dev/null || { echo "docker 없음"; exit 1; }
|
||||
docker compose version >/dev/null 2>&1 || { echo "docker compose v2 없음"; exit 1; }
|
||||
[ -f .env ] || { echo ".env 없음 — cp .env.example .env 후 채워"; exit 1; }
|
||||
set -a; . ./.env; set +a
|
||||
PORT="${BE_PORT:-8080}"; BASE="http://localhost:${PORT}/api/v1"
|
||||
|
||||
case "$CMD" in
|
||||
stop) docker compose down; echo "종료(볼륨 유지)"; exit 0 ;;
|
||||
logs) docker compose logs -f --tail=100; exit 0 ;;
|
||||
status) docker compose ps; curl -fsS -m 5 "$BASE/health" && echo || echo "헬스 응답 없음"; exit 0 ;;
|
||||
update) git -C "$HERE/../../.." pull --ff-only || { echo "git pull 실패 — 로컬 변경 확인"; exit 1; } ;;
|
||||
up) ;;
|
||||
*) echo "모르는 명령: $CMD (up|update|stop|logs|status)"; exit 1 ;;
|
||||
esac
|
||||
|
||||
[ -n "${OPENROUTER_API_KEY:-}${AAF_FABRIX_BASE_URL:-}" ] || { echo ".env 에 OPENROUTER_API_KEY(또는 AAF_FABRIX_*) 가 비어 있음"; exit 1; }
|
||||
|
||||
echo "-- 이미지 빌드 + 기동"
|
||||
docker compose up -d --build
|
||||
echo "-- 기동 확인(최대 2분)"
|
||||
for _ in $(seq 1 60); do curl -fsS -m 3 "$BASE/health" >/dev/null 2>&1 && break; sleep 2; done
|
||||
curl -fsS -m 5 "$BASE/health" || { echo "안 떴음 → ./deploy.sh logs"; docker compose ps; exit 1; }
|
||||
echo
|
||||
echo "-- OpenCode 설정(렌더 결과) 모델:"; grep -oE '"model": *"[^"]+"' ../../opencode/opencode.json || true
|
||||
echo "-- 앱 주소: http://$(hostname -I 2>/dev/null | awk '{print $1}'):${PORT}/api/v1"
|
||||
@@ -0,0 +1,35 @@
|
||||
# CodeAssist 백엔드 — 우리 환경(도커)용. 고객사(도커 없음)는 5_django_backend/deploy.sh.
|
||||
# 사용: 이 폴더에서 cp .env.example .env 채우고 ./deploy.sh
|
||||
# 밖으로 여는 건 be(:8080 → BE_PORT) 하나. 앱(Tauri)이 http://<서버>:<BE_PORT>/api/v1 로 붙음.
|
||||
name: codeassist
|
||||
|
||||
services:
|
||||
be:
|
||||
build: { context: ../.., dockerfile: deploy/docker/Dockerfile.be }
|
||||
ports:
|
||||
- "${BE_PORT:-8080}:8080"
|
||||
env_file: .env
|
||||
environment:
|
||||
OPENCODE_BASE_URL: http://opencode:4096
|
||||
OPENCODE_DIRECTORY: /workspace # OpenCode 컨테이너가 보는 workspace 경로 — 세션이 여기 생김
|
||||
BACKEND_HOST: be # 렌더된 opencode.json 의 게이트웨이 주소(고객사 모드일 때)
|
||||
BACKEND_PORT: "8080"
|
||||
MCP_HOST: ${MCP_HOST:-mcp}
|
||||
volumes:
|
||||
- ../../opencode:/app/opencode # AGENTS.md·.opencode·wiki·렌더 결과(opencode.json)
|
||||
- be_data:/app/data # SQLite(DB_HOST 비울 때)·auth secret
|
||||
depends_on: [opencode]
|
||||
restart: unless-stopped
|
||||
|
||||
opencode:
|
||||
build: { context: ../.., dockerfile: deploy/docker/Dockerfile.opencode }
|
||||
environment:
|
||||
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
|
||||
volumes:
|
||||
- ../../opencode:/workspace
|
||||
- oc_data:/root/.local/share/opencode # 세션·메시지 저장
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
be_data:
|
||||
oc_data:
|
||||
@@ -0,0 +1,22 @@
|
||||
# Phoenix — 우리 환경(도커)용. 한 대에 하나 띄우고 CodeAssist·ABAP_OPENCODE 둘 다 여기로 보냄(프로젝트로 나뉨).
|
||||
# 고객사(도커 없음)는 같은 폴더의 run.sh(pip 방식).
|
||||
# docker compose up -d → http://<서버>:6006
|
||||
name: phoenix
|
||||
|
||||
services:
|
||||
phoenix:
|
||||
image: arizephoenix/phoenix:latest
|
||||
ports:
|
||||
- "${PHOENIX_PORT:-6006}:6006"
|
||||
environment:
|
||||
PHOENIX_WORKING_DIR: /data
|
||||
PHOENIX_TELEMETRY_ENABLED: "false"
|
||||
# 로그인 켜려면 둘 다 채움(첫 관리자 admin@localhost / PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD). 켜면 수신에도 API 키 필요 — 모듈 헤더 한 줄 추가해야 함
|
||||
# PHOENIX_ENABLE_AUTH: "true"
|
||||
# PHOENIX_SECRET: ${PHOENIX_SECRET}
|
||||
volumes:
|
||||
- phoenix_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
phoenix_data:
|
||||
@@ -6,7 +6,7 @@
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "FabriX",
|
||||
"options": {
|
||||
"baseURL": "http://127.0.0.1:${BACKEND_PORT}/api/ito",
|
||||
"baseURL": "http://${BACKEND_HOST}:${BACKEND_PORT}/api/ito",
|
||||
"apiKey": "${AAF_GATEWAY_KEY}"
|
||||
},
|
||||
"models": ${AAF_FABRIX_MODELS_JSON}
|
||||
@@ -18,7 +18,7 @@
|
||||
"mcp": {
|
||||
"sap-icf": {
|
||||
"type": "remote",
|
||||
"url": "http://127.0.0.1:${MCP_PORT}/mcp",
|
||||
"url": "http://${MCP_HOST}:${MCP_PORT}/mcp",
|
||||
"headers": { "Authorization": "Bearer ${MCP_API_KEY}" },
|
||||
"enabled": ${SAP_MCP_ENABLED}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openrouter": {
|
||||
"options": { "apiKey": "{env:OPENROUTER_API_KEY}" },
|
||||
"models": {
|
||||
"z-ai/glm-5.2": { "name": "GLM 5.2" },
|
||||
"google/gemma-4-31b-it": { "name": "Gemma4", "attachment": true, "modalities": { "input": ["text", "image"], "output": ["text"] } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": "openrouter/z-ai/glm-5.2",
|
||||
"tools": { "question": false },
|
||||
"permission": { "external_directory": "allow" },
|
||||
"mcp": {
|
||||
"sap-icf": {
|
||||
"type": "remote",
|
||||
"url": "http://${MCP_HOST}:${MCP_PORT}/mcp",
|
||||
"headers": { "Authorization": "Bearer ${MCP_API_KEY}" },
|
||||
"enabled": ${SAP_MCP_ENABLED}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""고객사(FabriX)용 opencode.json 만들기 — 템플릿의 ${이름} 을 .env 값으로 채움.
|
||||
"""opencode.json 만들기 — 템플릿의 ${이름} 을 .env 값으로 채움.
|
||||
|
||||
python opencode/render_opencode.py # ../.env 읽어서 opencode/opencode.json 덮어씀
|
||||
python opencode/render_opencode.py --check # 쓰진 않고 결과만 출력
|
||||
|
||||
개발(OpenRouter)로 돌아가려면 `git checkout opencode/opencode.json`.
|
||||
템플릿 고르기: AAF_FABRIX_BASE_URL 이 있으면 고객사(FabriX 게이트웨이), 없으면 OpenRouter(우리 환경·로컬).
|
||||
호스트: BACKEND_HOST/MCP_HOST 기본 127.0.0.1, 도커 compose 에선 be / mcp 컨테이너 이름.
|
||||
ABAP_OPENCODE web/deploy/bare/render-opencode.py 와 같은 규칙:
|
||||
AAF_FABRIX_MODELS="605:Gemma4,339:GaussO Flash,581:GaussO Think" → 모델 목록. 키가 곧 x-llm-model-id.
|
||||
비어 있으면 AAF_FABRIX_MODEL_ID 하나(이름 FabriX). 기본 모델은 목록의 첫 번째.
|
||||
@@ -34,7 +35,7 @@ def load_env(path: Path) -> dict[str, str]:
|
||||
k, v = line.split("=", 1)
|
||||
v = v.split(" #", 1)[0].strip().strip("\"'") # 줄 끝 주석은 두 칸 띄고 #
|
||||
out[k.strip()] = v
|
||||
out.update({k: v for k, v in os.environ.items() if k.startswith(("AAF_", "BACKEND_PORT", "MCP_", "OPENCODE_"))})
|
||||
out.update({k: v for k, v in os.environ.items() if k.startswith(("AAF_", "BACKEND_", "MCP_", "OPENCODE_", "OPENROUTER_"))})
|
||||
return out
|
||||
|
||||
|
||||
@@ -53,7 +54,9 @@ def render(env: dict[str, str]) -> str:
|
||||
return m
|
||||
values["AAF_FABRIX_MODELS_JSON"] = json.dumps({mid: _model(mid, name) for mid, name in models.items()}, ensure_ascii=False)
|
||||
values["AAF_FABRIX_DEFAULT_MODEL"] = next(iter(models))
|
||||
values.setdefault("BACKEND_HOST", "127.0.0.1")
|
||||
values.setdefault("BACKEND_PORT", "8001")
|
||||
values.setdefault("MCP_HOST", "127.0.0.1")
|
||||
values.setdefault("MCP_PORT", "8888")
|
||||
# SAP MCP: MCP_API_KEY 가 있으면 켬. 없으면 끔(로컬 개발)
|
||||
values["SAP_MCP_ENABLED"] = "true" if values.get("MCP_API_KEY") else "false"
|
||||
@@ -67,7 +70,8 @@ def render(env: dict[str, str]) -> str:
|
||||
missing.append(m.group(1))
|
||||
return v
|
||||
|
||||
text = re.sub(r"\$\{(\w+)\}", fill, (HERE / "opencode.fabrix.json.tmpl").read_text(encoding="utf-8"))
|
||||
tmpl = "opencode.fabrix.json.tmpl" if env.get("AAF_FABRIX_BASE_URL") else "opencode.openrouter.json.tmpl"
|
||||
text = re.sub(r"\$\{(\w+)\}", fill, (HERE / tmpl).read_text(encoding="utf-8"))
|
||||
if missing:
|
||||
print("경고: 비어 있는 값 —", ", ".join(sorted(set(missing))), file=sys.stderr)
|
||||
return text
|
||||
|
||||
Reference in New Issue
Block a user