# Deploy — 에이전트용 전체 가이드 > Deploy(https://deploy.core.today)는 Git 저장소를 연결하면 빌드부터 배포, HTTPS 주소(`https://<이름>.apps.core.today`)까지 해 주는 컨테이너 배포 플랫폼입니다. 이 문서는 코딩 에이전트(LLM)가 사용자의 코드를 Deploy 에 배포하고, 빌드 결과를 확인해 고치는 데 필요한 규칙과 API 를 담고 있습니다. 짧은 판은 https://deploy.core.today/llms.txt, 사람이 읽는 가이드는 https://deploy.core.today/docs 에 있습니다. ## 1. 역할 나누기 | 누가 | 하는 일 | |---|---| | 사람 (콘솔 로그인) | 에이전트 로그인 승인(처음 한 번)·토큰 폐기, 프로젝트·데이터베이스 삭제, 커스텀 도메인, NAS 폴더, 알림(Slack·이메일), 미리보기 전용 환경변수, 웹훅 등록, 빌드 완료 콜백 주소·서명 시크릿, 롤백, 엔진 설정(LLM 키)이 필요한 데이터베이스 만들기. 원하면 에이전트가 하는 일도 전부 콘솔에서 할 수 있습니다 | | 에이전트 (에이전트 토큰 `kca_`) | 저장소를 배포 가능한 상태로 만들기, 프로젝트·데이터베이스 만들기와 연결, 환경변수·크기·복제본·GPU·포트·헬스체크·소스 설정 바꾸기, 빌드 시작·취소, 상태·빌드 로그·앱 로그 읽기, 배치 작업 | | CI·스크립트 (프로젝트 API 토큰 `kc_`) | 그 프로젝트 하나의 빌드 시작·취소, 상태·로그, 배치 작업 | 에이전트 토큰은 승인한 사람의 권한 그대로입니다. 조직 구성원은 프로젝트를 만들고 빌드할 수 있고, 남이 만든 프로젝트의 설정은 조직 관리자(소유자·관리자)만 바꿉니다 — API 도 같은 규칙으로 403 대신 409 와 이유를 돌려줍니다. 에이전트가 지킬 것: - 토큰(`kca_…`·`kc_…`)은 환경변수나 권한 600 파일(`~/.config/kubecore/agent-token`)로만 둡니다. 저장소·커밋·채팅 출력에 남기지 않습니다. 새어 나갔으면 `DELETE https://deploy.core.today/api/v1/auth/token` 으로 폐기하고(또는 사용자가 조직 화면에서) 다시 로그인합니다. - 프로젝트·데이터베이스를 만드는 것은 조직 예산을 씁니다. 사용자가 배포를 요청했을 때만 만들고, 이름(`slug`)이 겹치면 다른 사람의 것일 수 있으니 덮어쓰려 하지 말고 사용자에게 묻습니다. 실험으로 만든 것은 지울 수 없으니(삭제는 콘솔) 사용자에게 알려 줍니다. - 비밀값(API 키 등)은 사용자가 준 것만 `env` 로 넣고, 출력·커밋하지 않습니다. 데이터베이스 비밀번호는 플랫폼이 만들어 연결할 때 환경변수로 넣으니 에이전트가 볼 일이 없습니다. - 같은 오류로 빌드를 계속 다시 돌리지 않습니다. 빌드는 사용자의 크레딧을 씁니다. 원인을 고친 뒤에만 다시 시작합니다. - 삭제·도메인·NAS 는 API 에 없습니다. 필요하면 사용자에게 콘솔에서 해 달라고 합니다. ## 2. 낱말 - **프로젝트**: 서비스 하나 = 컨테이너 하나. 주소·설정·로그·빌드가 여기에 묶입니다. - **slug**: 프로젝트 ID. 만든 뒤 바꿀 수 없습니다. 주소는 따로 고를 수 있고(`address`, 기본은 `https://.apps.core.today`) 만든 뒤 사람이 콘솔에서 바꾸거나 더할 수 있으니, 주소는 slug 로 짐작하지 말고 응답의 `url` 을 씁니다. - **빌드**: 소스를 이미지로 만들어 배포까지 잇는 과정. `fetch`(소스 받기) → `build`(이미지 만들기·테스트) → `deploy`(교체) 세 단계입니다. - **복제본**: 같은 컨테이너의 개수(0~50). 0 이면 정지이고 요금이 나가지 않습니다. 2 이상이면 복제본이 서로 다른 서버에 나뉘어 서버 1대 장애에도 서비스가 유지됩니다(요금은 복제본 수에 비례). 복제본 1 은 서버 장애 때 1~2분 정도 끊긴 뒤 다른 서버에서 다시 뜹니다. - **조직**: 프로젝트·데이터베이스·예산(크레딧)의 소유 단위. 같은 조직의 프로젝트끼리만 내부 통신이 됩니다. ## 3. 저장소 준비 ### 3.1 모든 앱에 공통인 조건 - **`PORT` 환경변수의 포트에서 `0.0.0.0` 으로 HTTP 를 받습니다.** `localhost`/`127.0.0.1` 로만 열면 밖에서 닿지 않습니다. 프로젝트의 “컨테이너 포트” 값이 `PORT` 로 들어갑니다. - **HTTPS 는 플랫폼이 처리합니다.** 앱은 평문 HTTP 를 받고 `X-Forwarded-Proto: https` 헤더가 함께 옵니다. 앱에서 TLS 를 켜거나 http→https 리다이렉트를 직접 하지 않습니다. - **amd64 로 빌드됩니다.** ARM 전용 베이스 이미지는 쓸 수 없습니다. - **root 가 아닌 사용자로 도는 이미지는 1024 이상 포트를 씁니다.** - **디스크는 영구적이지 않습니다.** 컨테이너 안에 쓴 파일(업로드·SQLite 파일 포함)은 재시작·재배포와 함께 사라집니다. 데이터는 관리형 데이터베이스, 파일은 오브젝트 스토리지에 둡니다(9절). - **로그는 stdout/stderr 로 냅니다.** 파일에 쓴 로그는 수집되지 않습니다. - **요청 한 번의 크기는 100 MB 까지입니다.** 바깥에서 들어오는 길은 HTTP(S) 뿐이고 TCP/UDP 포트는 공개되지 않습니다. - **저장소 주소는 `https://` 형식입니다.** `git@…` SSH 주소는 지원하지 않습니다. 비공개 저장소는 읽기 전용 접근 토큰이 필요합니다(GitHub 라면 fine-grained 토큰에 `Contents: Read-only` 만). 같은 조직에 그 저장소 계정의 토큰을 저장한 프로젝트가 있으면 만들 때 `"gitTokenFrom": "<그 프로젝트 slug>"` 로 그대로 씁니다(에이전트는 값을 보지 않습니다). 없으면 사용자가 준 토큰을 `gitToken` 으로 넣거나, 사람이 프로젝트 폼의 “접근 토큰” 칸에 넣습니다. ### 3.2 Dockerfile 없이 — 자동 감지 빌드 (Railpack) 지정한 Dockerfile 경로에 파일이 없으면 저장소를 보고 언어·프레임워크·패키지 매니저를 감지해 빌드합니다. | 감지 대상 | 무엇을 보고 | 기본 동작 | |---|---|---| | Node.js | `package.json` | lockfile 로 npm·pnpm·yarn·bun 을 고르고 `engines`/`.nvmrc` 로 버전을 정함. Next·Nuxt·Vite·Astro·SvelteKit·Remix·Angular 인식. `build` → `start` 스크립트 실행 | | Python | `requirements.txt` · `pyproject.toml` | uv·poetry·pdm·pip. Django·FastAPI·Flask 시작 명령 추정 | | Go · Rust | `go.mod` · `Cargo.toml` | 바이너리를 빌드해 실행 | | Ruby · PHP · Java · .NET · Elixir · Deno · Bun | 각 매니페스트 | Rails·Laravel·Gradle/Maven 등 인식 | | 정적 사이트 | `index.html` 또는 SPA 빌드 결과 | 정적 파일 서버로 서빙 | - **lockfile 을 커밋합니다.** 패키지 매니저와 버전이 거기서 정해집니다. - **시작 명령은 `PORT` 로 열어야 합니다.** 예: `next start`(자동으로 읽음), `uvicorn app:app --host 0.0.0.0 --port $PORT`. - **설치·빌드·시작 명령을 덮어쓸 수 있습니다.** 프로젝트의 소스 설정(생성 링크의 `installCommand`·`buildCommand`·`startCommand`)이나 저장소의 `railpack.json` 으로 합니다. - **모노레포는 루트 디렉터리를 비워 둡니다.** 앱 폴더를 루트로 지정하면 워크스페이스 의존성을 찾지 못합니다. 저장소 루트에서 명령으로 앱을 고릅니다. 예: 빌드 `pnpm --filter @org/web... build`, 시작 `pnpm --filter @org/web start`. - **빌드 환경은 `NODE_ENV=production`, `CI=true` 입니다.** - **감지에 실패하면 fetch 단계에서 “자동 감지 실패”로 멈춥니다.** Dockerfile 을 추가하거나 빌드·시작 명령을 지정합니다. ### 3.3 Dockerfile 로 빌드 - 경로 기본값은 루트 디렉터리의 `Dockerfile` 입니다. 다른 위치면 프로젝트 소스 설정의 “Dockerfile 경로”(생성 링크의 `dockerfile`)에 적습니다. - 의존성 설치(`COPY package.json …` + `RUN npm ci`)를 소스 복사보다 앞에 두면 레이어 캐시가 다음 빌드에 재사용됩니다. - 컨테이너의 인자(args)·커맨드는 콘솔에서 바꿀 수 없습니다. 이미지의 `CMD` 가 서버를 띄워야 합니다. 남의 이미지에 인자를 줘야 하면 `FROM 이미지` + `CMD […]` 두 줄 Dockerfile 을 저장소에 둡니다. - **`USER` 로 비루트 사용자로 바꾸면 앱이 쓰는 폴더의 소유자도 바꿉니다.** 플랫폼은 실행 사용자를 바꾸지 않아 이미지의 `USER` 그대로 돕니다(로컬 `docker run` 과 같음). `COPY` 로 들어온 파일은 root 소유라 그 아래(캐시·업로드 폴더 등)에 쓰면 `Permission denied` 로 멈춥니다. `USER` 줄보다 앞에 `RUN mkdir -p <폴더> && chown <사용자> <폴더>` 를 두거나 `/tmp` 에 씁니다. 컨테이너 안에 쓴 파일은 재시작·재배포·서버 이동 때 사라집니다. ```dockerfile FROM node:24-alpine WORKDIR /app ARG NEXT_PUBLIC_API_URL # 빌드 시점 값만, 공개돼도 되는 것만 COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build ENV NODE_ENV=production CMD ["npm", "start"] # DATABASE_URL 같은 비밀값은 실행할 때 환경변수로 들어옴 ``` ### 3.4 환경변수 - 만들 때 `env`, 그 뒤엔 `PATCH https://deploy.core.today/api/v1/projects/` 의 `env` 로 넣습니다(`{"KEY":"값"}` 는 추가·변경, `{"KEY":null}` 은 삭제, 주지 않은 키는 그대로). 사람은 프로젝트 설정의 환경변수 칸에 `KEY=VALUE` 로 넣습니다. 비밀값으로 저장되고, 값을 바꾸면 자동으로 재배포됩니다. - **빌드할 때도 전달됩니다.** 자동 감지 빌드는 빌드 시크릿으로 넘겨 이미지 이력에 남지 않습니다. Dockerfile 빌드는 `--build-arg` 로 넘기고, Dockerfile 에 `ARG KEY` 로 선언한 것만 쓰입니다. - **Dockerfile 에서 비밀값을 `ARG` 로 받지 않습니다.** 이미지 이력에 남습니다. 비밀값은 실행할 때 환경변수로 들어오니 `ARG` 가 필요 없습니다. - **빌드 시점 값(`NEXT_PUBLIC_*`, `VITE_*` 등)은 이미지에 구워집니다.** 값을 바꾼 뒤에는 새 빌드를 시작해야 반영됩니다(`PATCH` 에 `"rebuild": true`). - `NVIDIA_VISIBLE_DEVICES` 는 지정할 수 없습니다. GPU 는 프로젝트 설정의 GPU 개수·종류로만 붙습니다. - API 의 프로젝트 상태(`envKeys`)로 어떤 이름이 설정돼 있는지 확인할 수 있습니다(값은 나오지 않습니다). ### 3.5 테스트 명령 — 배포 전 검문소 프로젝트 소스 설정의 “테스트 명령”(생성 링크의 `testCommand`)을 적어 두면 빌드 중에 테스트가 먼저 돌고, 실패하면 빌드가 실패로 끝나 **배포되지 않습니다.** - 자동 감지 빌드: 의존성 설치 뒤·빌드 명령 앞에 실행됩니다(devDependencies 포함). 환경이 `NODE_ENV=production` 이라, React Testing Library·vitest 처럼 개발 빌드가 필요한 테스트는 `NODE_ENV=test pnpm test` 처럼 앞에 붙입니다. - Dockerfile 빌드: 완성된 이미지 안에서 실행됩니다. 멀티스테이지로 소스·테스트 의존성을 뺀 이미지에서는 돌릴 수 없으니, 그럴 땐 Dockerfile 의 빌드 스테이지에 `RUN npm test` 를 직접 둡니다. - 외부 DB 가 필요한 테스트는 인메모리 대체 수단을 쓰거나 제외합니다. ### 3.6 헬스체크 - 경로를 지정하면 그 경로가 200 을 돌려줄 때만 요청을 받고, 새 버전이 준비된 뒤에야 옛 버전을 내립니다(무중단 배포). - 인증이 필요 없고, 리다이렉트하지 않고, 가벼운 전용 경로를 둡니다. 예: `/health`, `/api/health`. - 비워 두면 포트에 TCP 연결이 되는지만 확인합니다. HTTP 가 아닌 서비스(gRPC 등)는 비워 둡니다. - 기동이 90초 넘게 걸리는 앱(모델 로딩 등)은 헬스체크 실패로 보일 수 있습니다. ### 3.7 LLM 서빙 (Ollama·vLLM) - GPU 서버에 직접 모델 서버를 띄우지 않습니다. 프로젝트(상시 서비스)나 배치 작업(일회성 대량 추론)으로 GPU 를 배정받습니다. 템플릿 `ollama`·`vllm` 이 이미지·포트·크기·GPU 를 채웁니다. - GPU 종류는 모델 파일 크기 + 2~4GB 이상의 메모리를 가진 것을 고릅니다. 모자라면 일부가 CPU 로 넘어가 매우 느려집니다(Ollama 는 `/api/ps` 의 `size_vram` 이 `size` 와 같은지로 확인). - 공개 주소로 들어오는 요청은 응답 바이트 없이 60초가 지나면 끊깁니다(504). 긴 생성은 `stream: true` 로 받습니다. - 컨테이너 디스크는 재시작·재배포·서버 이동 때 비워집니다. Ollama 로 받은 모델을 남기려면 Dockerfile 에서 빌드 중에 받아 이미지에 넣습니다(`RUN (ollama serve &) && until ollama list; do sleep 1; done && ollama pull <모델>`, 빌드 제한 시간 안에 받을 수 있는 크기). - Ollama 에는 인증이 없고 공개 주소는 끌 수 없습니다. 같은 조직 프로젝트끼리는 내부 주소로 부르고, 바깥에서 써야 하면 `VLLM_API_KEY` 를 거는 vLLM 을 씁니다. V100 에서 vLLM 은 `--dtype half` 가 필요합니다. - GPU 조각을 고른 프로젝트는 그 GPU 가 있는 서버에 묶여 서버 장애 때 옮겨지지 않습니다. 장애에도 버텨야 하면 통째 GPU 를 고릅니다. ## 4. 프로젝트 만들기 ### 4.1 API 로 — `POST https://deploy.core.today/api/v1/projects` (에이전트 토큰) ```bash curl -fsS -X POST "https://deploy.core.today/api/v1/projects" -H "Authorization: Bearer $KUBECORE_AGENT_TOKEN" -H "Content-Type: application/json" -d '{ "slug": "acme-shop", "name": "Acme Shop", "repo": "https://github.com/acme/shop", "ref": "main", "port": 3000, "healthPath": "/api/health", "size": "s", "env": { "OPENAI_API_KEY": "sk-…", "NEXT_PUBLIC_SITE_URL": "https://acme-shop.apps.core.today" }, "gitTokenFrom": "acme-api" }' ``` | 필드 | 뜻 | |---|---| | `slug` | 필수. 프로젝트 ID. 규칙은 4.3 표의 `slug` 와 같음 | | `address` | 주소 이름 — `https://
.apps.core.today`. 생략하면 slug. 다른 프로젝트가 쓰거나 보류 중인 이름이면 409 (4.3 표의 `address`). 작업자(port 0)는 무시 | | `repo` | `https://` Git 저장소 주소. `image` 와 둘 중 하나 | | `image` | 이미 만든 공개 이미지(고정 태그). `repo` 와 둘 중 하나 — 빌드 API 를 못 씀 | | `port` | 필수. 앱이 여는 포트 (같은 값이 `PORT` 환경변수로 들어감). `0` 이면 작업자 — 요청을 받지 않고 계속 도는 프로세스(9.8). 만든 뒤 웹↔작업자는 못 바꿈 | | `name` | 표시 이름. 기본은 slug | | `ref` | 배포할 브랜치. 기본 `main` | | `context` | 루트 디렉터리. 기본은 저장소 루트 | | `dockerfile` | Dockerfile 경로. 기본 `Dockerfile` — 없으면 자동 감지 빌드 | | `installCommand · buildCommand · startCommand · testCommand` | 자동 감지 빌드의 명령 덮어쓰기(startCommand 는 Dockerfile 빌드에도 적용)·테스트 명령 (3.2·3.5) | | `healthPath` | 헬스체크 경로. 기본 없음(포트 연결만 확인) | | `size` | `xs` · `s` · `m` · `l` · `xl` · `xxl`. 기본 `s` (4.4) | | `replicas` | 복제본 0~50. 기본 1 | | `gpu · gpuModel` | GPU 개수(0~4)와 종류. 기본 0 | | `env` | `{"KEY":"값"}` 환경변수 (3.4). 비밀값도 여기로 — 응답에는 이름만 나옴 | | `autoDeploy` | 빌드 성공 시 자동 배포. 기본 `true` | | `previews` | 브랜치 미리보기 (9.1). 기본 `false` | | `gitTokenFrom` | 비공개 저장소: 같은 조직의 이 프로젝트에 저장된 접근 토큰을 복사해 씀 (그 프로젝트의 설정 권한 필요) | | `gitToken` | 비공개 저장소: 읽기 전용 접근 토큰 원문 (사용자가 준 것만) | 응답(201): `{"ok":true,"project":{…6.1 과 같은 모양},"build":{"id","statusUrl","logsUrl"},"buildError":null}`. Git 저장소면 첫 빌드가 바로 시작됩니다 — `build.statusUrl` 을 폴링합니다(6.3). `buildError` 가 있으면 프로젝트는 만들어졌으니 `POST …/builds` 로 다시 시작합니다. - 같은 `slug` 가 이미 있으면 409 입니다(플랫폼 전체에서 하나뿐). 다른 이름을 고르거나 사용자에게 묻습니다. - 새 프로젝트는 조직 안에서만 만들 수 있고, 그 조직에 예산(크레딧)이 있어야 합니다(없으면 402·409 와 이유). - 만든 뒤 주소는 응답의 `url`(기본은 `https://.apps.core.today`, `address` 를 줬으면 그 이름)입니다. 내부에서만 쓸 서비스에도 공개 주소가 생기고 끌 수 없으니, 인증이 없는 앱은 앱 자체의 인증을 켭니다. 요청을 받지 않는 프로세스라면 `"port": 0`(작업자, 9.8)으로 만들어 주소 자체를 두지 않습니다. ### 4.2 설정 바꾸기 — `PATCH https://deploy.core.today/api/v1/projects/` (에이전트 토큰) 준 필드만 바뀝니다. 런타임 설정(환경변수·크기·복제본·포트·헬스체크)은 롤링 재배포로 바로 반영되고, 빌드 설정(브랜치·Dockerfile·명령)과 빌드 시점 값은 `"rebuild": true` 를 주면 같은 요청에서 새 빌드를 시작합니다. ```bash curl -fsS -X PATCH "https://deploy.core.today/api/v1/projects/acme-shop" -H "Authorization: Bearer $KUBECORE_AGENT_TOKEN" -H "Content-Type: application/json" \ -d '{"env":{"NEXT_PUBLIC_API_URL":"https://api.acme.example","OLD_FLAG":null},"size":"m","rebuild":true}' ``` 받는 필드: `name` · `port` · `healthPath` · `size` · `replicas` · `gpu` · `gpuModel` · `env` · `repo` · `ref` · `context` · `dockerfile` · `installCommand` · `buildCommand` · `startCommand` · `testCommand` · `autoDeploy` · `previews` · `gitToken` · `gitTokenFrom` · `rebuild` (뜻은 4.1 표와 같음). 그 밖의 필드(도메인·NAS·알림)는 400 입니다. 응답: `{"ok":true,"changed":[…],"redeployed":true,"project":{…},"build":{…}|null}`. 남이 만든 프로젝트는 조직 관리자만 바꿀 수 있습니다(409). ### 4.3 생성 링크로 — 사람이 확인하고 만들 때 에이전트 로그인을 쓰지 않을 때는 저장소를 분석해 **값이 채워진 생성 링크**를 사용자에게 줍니다. 사용자가 링크를 열어 값을 확인하고 “프로젝트 만들기”를 누르면, Git 프로젝트는 첫 빌드가 바로 시작됩니다. ``` https://deploy.core.today/projects/new?mode=git&repo=https%3A%2F%2Fgithub.com%2Facme%2Fshop&ref=main&slug=acme-shop&name=Acme%20Shop&port=3000&healthPath=%2Fapi%2Fhealth&size=s&env=DATABASE_URL,OPENAI_API_KEY ``` | 파라미터 | 뜻 | |---|---| | `mode` | `git`(저장소에서 빌드) 또는 `image`(이미 만든 공개 이미지). 생략하면 `repo` 가 있을 때 `git`, `image` 가 있을 때 `image` | | `repo` | `https://` 로 시작하는 저장소 주소 (`.git` 생략 가능) | | `ref` | 배포할 브랜치. 기본 `main` | | `slug` | 프로젝트 ID. 소문자·숫자·하이픈, 2~30자, 하이픈으로 시작·끝 불가, 하이픈 연속 불가. 플랫폼 전체에서 하나뿐이고 만든 뒤 못 바꿈. `address` 를 주지 않으면 주소도 이 이름 | | `address` | 주소 이름 — `https://
.apps.core.today`. 생략하면 slug. 소문자·숫자·하이픈 1~63자, 하이픈 연속 불가. 다른 프로젝트가 쓰거나 보류 중인 이름은 못 씀. 만든 뒤 사용자가 콘솔에서 바꾸거나 더할 수 있음 | | `name` | 표시 이름 (60자 이내, 나중에 바꿀 수 있음) | | `image` | `mode=image` 일 때 이미지 주소. 고정 태그를 씁니다(예 `ghcr.io/org/app:1.2.0`) | | `port` | 앱이 여는 포트 (같은 값이 `PORT` 환경변수로 들어감). 기본 3000. `0` 이면 작업자(포트·주소 없음, 9.8) | | `size` | 크기: `xs` · `s` · `m` · `l` · `xl` · `xxl`. 기본 `s` | | `healthPath` | 헬스체크 경로 (`/` 로 시작). 생략하면 포트 연결만 확인 | | `context` | 루트 디렉터리 (저장소 안 상대 경로). 모노레포는 비워 두고 빌드·시작 명령을 지정하는 편이 안전합니다 | | `dockerfile` | Dockerfile 경로 (루트 디렉터리 기준). 기본 `Dockerfile`. 그 파일이 없으면 자동 감지 빌드 | | `installCommand` | 자동 감지 빌드의 설치 명령 덮어쓰기 | | `buildCommand` | 자동 감지 빌드의 빌드 명령 덮어쓰기 | | `startCommand` | 시작 명령 덮어쓰기 (웹 서비스는 `$PORT` 로 열어야 함). 자동 감지 빌드와 Dockerfile 빌드 모두 — Dockerfile 이면 `CMD` 를 대신함 (9.8) | | `testCommand` | 테스트 명령. 실패하면 배포하지 않음 | | `previews` | `1` 이면 브랜치 미리보기 환경을 켬 | | `env` | 환경변수 **이름**만 쉼표로 (예 `DATABASE_URL,OPENAI_API_KEY`). 폼에 `이름=` 줄로 채워지고 값은 사용자가 직접 넣습니다. 값은 링크에 넣을 수 없습니다 | - 값은 모두 URL 인코딩합니다. 형식이 맞지 않는 값은 무시되고 폼의 기본값이 쓰입니다. - 링크는 폼을 채우기만 합니다. 사용자가 저장하기 전에는 아무것도 만들어지지 않습니다. 링크에는 환경변수 값을 넣을 수 없습니다. ### 4.4 크기 크기(`size`)는 보장량이자 상한입니다. 메모리를 넘기면 강제 종료(OOMKilled)됩니다. | size | CPU · 메모리 | |---|---| | `xs` (XS) | 0.25 vCPU · 256 MB | | `s` (S) | 0.5 vCPU · 512 MB | | `m` (M) | 1 vCPU · 1 GB | | `l` (L) | 2 vCPU · 4 GB | | `xl` (XL) | 4 vCPU · 8 GB | | `xxl` (2XL) | 8 vCPU · 16 GB | 프로젝트 하나의 합계 한도는 CPU 236 · 메모리 880Gi · GPU 4 · Pod 300 이고(복제본 0~50, GPU 0~4), 조직 한도도 함께 검사합니다. ## 5. 토큰 ### 5.1 에이전트 로그인 (에이전트 토큰 `kca_`) `vercel login` 과 같은 방식입니다. 사람은 링크를 열어 “승인”만 누릅니다. ```bash # 1) 시작 — name 은 승인 화면에 보일 이름 curl -fsS -X POST "https://deploy.core.today/api/v1/auth/device" -H "Content-Type: application/json" -d '{"name":"Claude Code @ macbook"}' # → {"deviceCode":"kcd_…","userCode":"BCDF-GHJK","verificationUrl":"https://deploy.core.today/device?code=BCDF-GHJK","expiresIn":600,"interval":3} # 2) 사용자에게 verificationUrl 과 userCode 를 보여 준다 (deviceCode 는 보여 주지 않는다) # 3) interval 초마다 결과를 묻는다 curl -sS -X POST "https://deploy.core.today/api/v1/auth/token" -H "Content-Type: application/json" -d '{"deviceCode":"kcd_…"}' # 대기: 200 {"status":"pending"} 거절: 403 {"status":"denied"} 만료·이미 받음: 410 {"status":"expired"} # 승인: 200 {"status":"approved","token":"kca_…","expiresAt":"…","user":{"email":"…"},"org":{"slug":"…","name":"…"}} # 4) 저장 (토큰은 이 응답에 한 번만 나온다) mkdir -p ~/.config/kubecore && (umask 077 && printf %s "kca_…" > ~/.config/kubecore/agent-token) ``` - 사람은 승인 화면에서 **조직**과 **유효 기간**(1·7·30·90일, 기본 7일)을 고릅니다. 토큰은 그 조직 하나에서, 승인한 사람의 그 조직 역할로 동작합니다. 그 사람이 조직에서 빠지거나 조직이 정지되면 바로 막힙니다. - 쓰기 전에 `GET https://deploy.core.today/api/v1/me` 로 확인합니다: `{"user":{"email"},"org":{"slug","name","role"},"token":{"id","label","expiresAt"}}`. 401 이면 만료·폐기된 것이니 다시 로그인합니다. - 사용자는 조직 화면의 “에이전트 토큰”에서 언제든 폐기할 수 있습니다. 에이전트 스스로는 `DELETE https://deploy.core.today/api/v1/auth/token` 로 로그아웃합니다. - 다른 조직에 배포해야 하면 다시 로그인하고 승인 화면에서 그 조직을 고르게 합니다. ### 5.2 프로젝트 API 토큰 (`kc_`, CI 용) 1. 프로젝트의 **“배치 작업” 탭 → “API” 카드 → “토큰 발급”** (조직 관리자나 그 프로젝트를 만든 사람만 할 수 있습니다). 2. `kc_` 로 시작하는 값을 CI 시크릿이나 셸 환경변수로 넘깁니다: `export KUBECORE_API_TOKEN=kc_…` 3. 토큰은 **그 프로젝트 하나**의 빌드·로그·작업(6.1~6.6)에만 통합니다. 만들기·설정 바꾸기(4.1·4.2)·데이터베이스는 되지 않습니다. ## 6. API - 기준 주소: `https://deploy.core.today/api/v1/projects/` (6.1~6.6 은 프로젝트 하나에 대한 것) - 인증: 모든 요청에 `Authorization: Bearer <토큰>` — 에이전트 토큰(`$KUBECORE_AGENT_TOKEN`)이나 그 프로젝트의 API 토큰(`$KUBECORE_API_TOKEN`) - 응답: 로그 두 가지(`…/logs`)는 `text/plain`, 나머지는 JSON. 오류는 `{"error":"<사람이 읽는 한 줄>"}` | 상태 코드 | 뜻 | |---|---| | 200 · 202 | 성공 (202 는 빌드·작업이 시작됨) | | 400 | 요청 값이 틀림 (`error` 에 이유) | | 401 | 토큰이 없거나 틀림·만료·폐기, 또는 그 토큰으로 볼 수 없는 slug (둘을 구분해 주지 않습니다) | | 402 | 조직 예산이 없거나 소진됨. 사용자가 플랫폼 관리자에게 예산을 요청해야 합니다 | | 404 | 그 빌드·작업이 없음 | | 409 | 지금은 할 수 없음 (이미 있는 이름, 설정 권한 없음, Git 소스가 없는 프로젝트, 이미 끝난 빌드, 한도 초과 등 — `error` 에 이유) | ### 6.1 프로젝트 상태 — `GET https://deploy.core.today/api/v1/projects/` ```json { "project": { "slug": "acme-shop", "displayName": "Acme Shop", "url": "https://acme-shop.apps.core.today", "hosts": ["acme-shop.apps.core.today"], "phase": "running", "phaseLabel": "실행 중", "message": null, "replicas": { "desired": 1, "ready": 1, "updated": 1 }, "kind": "web", "port": 3000, "size": "s", "gpu": 0, "healthPath": "/api/health", "envKeys": ["DATABASE_URL", "OPENAI_API_KEY"], "source": { "repo": "https://github.com/acme/shop", "ref": "main", "context": "", "dockerfile": "Dockerfile", "autoDeploy": true, "previews": false, "testCommand": "" }, "currentBuild": "20261013-091530-a1b2", "lastBuild": { "id": "20261013-091530-a1b2", "stage": "succeeded", "ref": "main", "commit": "3f2c1ab…", "message": null, "createdAt": "2026-10-13T00:15:30.000Z" }, "lastDeployedAt": "2026-10-13T00:17:02.000Z", "consoleUrl": "https://deploy.core.today/projects/acme-shop" } } ``` - `phase` 의 뜻과 할 일은 7절에 있습니다. `message` 는 원인을 사람 말로 푼 한 줄입니다. - `source` 가 `null` 이면 이미지 직접 지정 프로젝트라 빌드 API 를 쓸 수 없습니다. - `source.autoDeploy` 가 `false` 면 빌드가 성공해도 배포하지 않습니다. 사용자가 콘솔의 빌드 목록에서 “배포”를 눌러야 합니다. ### 6.2 빌드 시작 — `POST https://deploy.core.today/api/v1/projects//builds` 본문은 생략할 수 있습니다(설정된 브랜치로 빌드). ```bash curl -fsS -X POST "$BASE/builds" -H "Authorization: Bearer $KUBECORE_API_TOKEN" # 브랜치·태그·커밋 지정 curl -fsS -X POST "$BASE/builds" -H "Authorization: Bearer $KUBECORE_API_TOKEN" -H "Content-Type: application/json" -d '{"ref":"v1.4.0"}' ``` ```json { "ok": true, "build": "20261013-091530-a1b2", "preview": false, "callback": false, "statusUrl": "https://deploy.core.today/api/v1/projects/acme-shop/builds/20261013-091530-a1b2", "logsUrl": "https://deploy.core.today/api/v1/projects/acme-shop/builds/20261013-091530-a1b2/logs" } ``` - 같은 프로젝트에 새 빌드를 시작하면 진행 중이던 빌드는 취소되고 “새 빌드 … 로 대체됨”으로 남습니다. 정상입니다. - 저장소에 웹훅이 등록돼 있으면 push 만으로 빌드가 시작됩니다. 그때는 `POST` 대신 `GET …/builds?limit=1` 로 방금 시작된 빌드를 찾습니다. - `callbackUrl`(공개 인터넷의 `https://` 주소)을 주면 이 빌드가 끝났을 때 그 주소로 결과를 보냅니다(6.7). 응답의 `callback` 이 `true` 가 됩니다. ### 6.3 빌드 상태 — `GET https://deploy.core.today/api/v1/projects//builds/` ```json { "build": { "id": "20261013-091530-a1b2", "stage": "failed", "stageLabel": "실패", "done": true, "failedStage": "build", "message": "빌드 단계 실패 (exit 1)", "summary": "src/app/page.tsx(12,7): error TS2322: Type 'string' is not assignable to type 'number'.", "ref": "main", "commit": "3f2c1ab…", "commitMessage": "fix cart total", "trigger": "api", "autoDeploy": true, "deployed": false, "deployNote": null, "preview": null, "createdAt": "2026-10-13T00:15:30.000Z", "finishedAt": "2026-10-13T00:16:41.000Z", "durationSec": 71, "logsUrl": "https://deploy.core.today/api/v1/projects/acme-shop/builds/20261013-091530-a1b2/logs", "consoleUrl": "https://deploy.core.today/projects/acme-shop/builds/20261013-091530-a1b2", "callbacks": [] } } ``` - `stage`: `pending` → `fetching` → `building` → `deploying` → `succeeded` | `failed` | `canceled` - `done` 이 `true` 가 될 때까지 5~10초 간격으로 확인합니다. 보통 1~3분(첫 빌드는 1~2분 더), 최대 30분입니다. - 실패하면 `failedStage`(어느 단계)·`message`(한 줄 원인)·`summary`(테스트 실패 항목·오류 직전 로그)를 먼저 읽습니다. - `succeeded` 인데 `deployed` 가 `false` 면 `deployNote` 에 이유가 있습니다(자동 배포가 꺼져 있거나, 더 새 빌드가 이미 배포됨). - 목록은 `GET https://deploy.core.today/api/v1/projects//builds?limit=10` (`{"builds":[…]}`, 새것부터), 취소는 `DELETE https://deploy.core.today/api/v1/projects//builds/` 입니다. - `callbacks` 는 완료 콜백 전송 상태입니다(보낼 곳이 없었으면 빈 배열). 6.7 참고. ### 6.4 빌드 로그 — `GET https://deploy.core.today/api/v1/projects//builds//logs` `text/plain` 으로 마지막 줄들을 돌려줍니다. - `?stage=fetch|build|deploy` — 생략하면 실패한 단계(진행 중이면 지금 단계) - `?tail=N` — 마지막 N줄 (기본 300, 최대 5000) ### 6.5 앱 로그 — `GET https://deploy.core.today/api/v1/projects//logs` 지금 떠 있는 모든 복제본의 마지막 줄들을 `[<복제본 접미사>] ` 프리픽스로 합쳐 `text/plain` 으로 돌려줍니다. - `?tail=N` — 복제본마다 마지막 N줄 (기본 300, 최대 5000) - `?previous=1` — 재시작 **직전** 컨테이너의 로그. 시작하자마자 죽는 앱(`crashloop`)의 원인은 여기에 있습니다 - `?timestamps=1` — 줄마다 시각을 붙임 ### 6.6 한 바퀴 예시 ```bash BASE=https://deploy.core.today/api/v1/projects/acme-shop AUTH="Authorization: Bearer $KUBECORE_API_TOKEN" ID=$(curl -fsS -X POST "$BASE/builds" -H "$AUTH" | jq -r .build) while :; do B=$(curl -fsS "$BASE/builds/$ID" -H "$AUTH") [ "$(echo "$B" | jq -r .build.done)" = "true" ] && break sleep 8 done echo "$B" | jq '.build | {stage, failedStage, message, summary}' # 실패했다면: 원인 로그 curl -fsS "$BASE/builds/$ID/logs?tail=200" -H "$AUTH" # 성공했다면: 앱이 실제로 떴는지 curl -fsS "$BASE" -H "$AUTH" | jq '.project | {phase, message, url}' curl -fsS -o /dev/null -w '%{http_code}\n' "https://acme-shop.apps.core.today/api/health" ``` ### 6.7 빌드 완료 콜백 폴링 대신, 빌드가 끝나면(`succeeded`·`failed`·`canceled`) Deploy 가 정해 둔 주소로 결과를 `POST` 합니다. 보낼 곳은 두 가지이고 둘 다 있으면 둘 다 보냅니다. - **프로젝트 콜백**: 사람이 프로젝트 설정 → “빌드 완료 콜백”에 저장한 주소. push·콘솔·API 로 시작한 모든 빌드에 보냅니다. 저장할 때 `ping` 이 2xx 를 받아야 저장됩니다. - **빌드별 콜백**: `POST …/builds` 본문의 `callbackUrl`. 그 빌드에만 보냅니다. 주소는 공개 인터넷의 `https://` 만 됩니다(사설·내부 주소로 풀리는 이름, 리다이렉트는 거절·실패). 요청은 이렇게 생겼습니다. ```http POST /your/path HTTP/1.1 Content-Type: application/json User-Agent: kubecore-callback/1 X-Kubecore-Event: build.finished X-Kubecore-Delivery: 20261013-091530-a1b2 X-Kubecore-Signature: t=1760318201,v1=5f0c…(hex) {"event":"build.finished","project":"acme-shop","build":{ …6.3 의 build 와 같은 필드(callbacks 제외)… }} ``` - **서명**: `v1` = HMAC-SHA256(서명 시크릿, `".<본문 원문>"`) 의 hex. 받는 쪽은 본문을 파싱하기 전에 원문으로 계산해 상수 시간 비교하고, `t` 가 지금과 5분 넘게 차이 나면 버립니다. 서명 시크릿(`kcw_…`)은 사람이 같은 설정 카드에서 보고 재발급합니다. 프로젝트와 빌드별 콜백이 같은 시크릿을 씁니다. 에이전트는 시크릿을 묻거나 저장하지 않습니다 — 받는 시스템의 환경변수에 넣어 달라고 사용자에게 요청합니다. - **재시도**: 2xx 가 아니거나 연결이 안 되면 30초·2분·10분·30분·1시간·2시간 뒤 다시 보냅니다(최대 7번, 10초 안에 응답해야 함). 410 이나 공개 주소가 아니면 바로 멈춥니다. 재시도는 본문이 같고 `t`·서명만 바뀝니다. - **중복**: 같은 빌드가 두 번 올 수 있습니다(응답이 늦어 재시도된 경우). `X-Kubecore-Delivery`(= 빌드 ID)로 걸러 냅니다. - **ping**: 설정 저장·테스트 버튼은 `X-Kubecore-Event: ping`, 본문 `{"event":"ping","project":"…","sentAt":"…"}` 을 같은 방식으로 서명해 보냅니다. 2xx 로 답하면 됩니다. - 전송 결과는 빌드 상태의 `callbacks` 에 있습니다: `target`(project|build)·`state`(pending|sent|failed)·`attempts`·`lastStatus`·`lastError`·`lastAttemptAt`·`nextAttemptAt`. ```js // Node (Express) — raw body 로 받아 검증 app.post("/hooks/deploy", express.raw({ type: "application/json" }), (req, res) => { const sig = Object.fromEntries(String(req.get("x-kubecore-signature")).split(",").map((p) => p.split("="))); const expected = crypto.createHmac("sha256", process.env.KUBECORE_CALLBACK_SECRET).update(`${sig.t}.${req.body}`).digest("hex"); const ok = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300 && expected.length === sig.v1?.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1)); if (!ok) return res.sendStatus(401); const msg = JSON.parse(req.body); if (msg.event === "build.finished") console.log(msg.build.id, msg.build.stage, msg.build.deployed); res.sendStatus(204); }); ``` ## 7. 상태(phase)별 대처 | phase | 뜻 | 에이전트가 할 일 | |---|---|---| | `running` | 실행 중 | 끝. 공개 주소를 확인해 사용자에게 알려 줍니다. | | `deploying` | 배포 중 | 새 버전이 올라오는 중. 10초 간격으로 다시 확인합니다. 5분(GPU 프로젝트 15분) 안에 끝나지 않으면 `failed` 가 됩니다. | | `awaiting` | 첫 빌드 대기 | 첫 빌드가 아직 성공하지 않았습니다. 최근 빌드의 상태와 로그를 봅니다. | | `degraded` | 일부 장애 | 일부 복제본만 준비됐습니다. 앱 로그를 보고, 자원이 모자라면 크기·복제본 조정을 사용자에게 제안합니다. | | `unhealthy` | 헬스체크 실패 | 앱은 떠 있는데 헬스체크 경로가 200 을 돌려주지 않습니다. 경로·포트·인증 여부를 확인합니다. | | `crashloop` | 반복 종료 | 시작 직후 종료를 반복합니다. `GET …/logs?previous=1` 로 직전 컨테이너의 첫 오류를 봅니다. 보통 `PORT` 를 쓰지 않거나 필수 환경변수가 빠진 경우입니다. | | `image-error` | 이미지 오류 | 이미지를 받을 수 없습니다. 이미지 직접 지정 프로젝트라면 주소·태그·공개 여부·amd64 여부를 확인합니다. | | `oom` | 메모리 초과 | 메모리 한도를 넘었습니다. 워커 수를 줄이거나, 크기를 한 단계 올리도록 사용자에게 요청합니다. | | `failed` | 실패 | 배포가 5분(GPU 프로젝트 15분) 안에 끝나지 않았습니다. 앱 로그로 원인을 찾고, 급하면 사용자가 콘솔에서 이전 리비전으로 롤백할 수 있습니다. | | `stopped` | 정지 | 복제본이 0 입니다(정지). 사용자가 콘솔에서 복제본을 올려야 다시 뜹니다. | 빌드가 `succeeded` 여도 앱이 곧 죽을 수 있으니, 배포 뒤에는 프로젝트 상태를 한 번 더 확인합니다. ## 8. 빌드 실패 — 단계별 원인 | failedStage | 흔한 원인 | 고치는 법 | |---|---|---| | `fetch` | 저장소 주소·브랜치 오타, 접근 토큰 없음·만료·권한 부족(Fine-grained 토큰에 그 저장소가 선택되지 않음 포함), 루트 디렉터리 오타, Dockerfile 이 없는데 자동 감지도 실패(앱이 하위 폴더에 있으면 루트 디렉터리 지정) | 주소·브랜치 확인. 토큰은 사용자가 다시 발급해 설정에 저장. 감지 실패면 Dockerfile 을 추가하거나 빌드·시작 명령 지정 | | `build` | Dockerfile 문법·명령 오류, 컴파일·타입 오류, 빌드 시점 값을 `ARG` 로 선언하지 않음, lockfile 없음, 30분 초과, 메모리 16 GiB 초과 | 로그의 마지막 실패 명령을 보고 코드 수정. 필요한 키를 `ARG` 로 선언. lockfile 커밋. 멀티스테이지로 산출물만 담기 | | `build` (테스트 실패) | 테스트가 실제로 실패, 자동 감지 빌드의 `NODE_ENV=production` 때문에 테스트가 깨짐, 외부 DB 가 필요한 테스트 | `summary` 의 FAIL 항목 확인. 테스트 명령 앞에 `NODE_ENV=test` 붙이기 | | `deploy` | 이미지는 만들었지만 5분(GPU 프로젝트 15분) 안에 앱이 준비되지 않음 — `PORT` 미사용, 필수 환경변수 누락, 헬스체크 경로가 200 이 아님(404 등)·5초 안에 응답하지 않음, 메모리 초과 | `error` 가 사유(종료 반복·헬스체크 상태 코드·시간 초과·연결 거부)를 구분한다. deploy 로그 끝에 직전 실행 로그가 있고 앱 로그(`?previous=1`)도 확인. 코드 문제면 고쳐서 다시 빌드, 설정 문제면 사용자에게 요청 | 빌드 자원은 CPU 8 · 메모리 16 GiB · 작업공간 20 GiB · 제한 시간 30분입니다. ## 9. 그 밖의 기능 ### 9.1 브랜치 미리보기 프로젝트 소스 설정에서 “브랜치 미리보기 환경”을 켠 프로젝트는 프로덕션 브랜치가 아닌 브랜치를 별도 주소에 띄울 수 있습니다. ```bash curl -fsS -X POST "$BASE/builds" -H "$AUTH" -H "Content-Type: application/json" -d '{"ref":"feature/login","preview":true}' ``` - 주소는 `--<브랜치>-<해시 4자>.apps.core.today` 형식이고, 빌드 상태의 `preview.url` 로 돌아옵니다. - 프로덕션과 같은 설정(크기·환경변수·헬스체크)으로 뜹니다. 설정의 “미리보기 환경변수”에 적은 키는 미리보기에서만 프로덕션 값을 덮어씁니다. - 미리보기를 켜지 않은 프로젝트나 프로덕션 브랜치로는 만들 수 없습니다(409). ### 9.2 push 마다 자동 배포 (웹훅) 사람이 프로젝트 설정 → 소스의 웹훅 URL 을 복사해 저장소의 Webhooks 에 등록하면(Content type `application/json`, push 이벤트만) 설정한 브랜치에 push 할 때마다 빌드·배포됩니다. URL 안의 토큰은 비밀값이니 에이전트가 묻거나 저장하지 않습니다. ### 9.3 배치 작업 웹 서버처럼 떠 있는 게 아니라 일을 마치면 사라지는 실행입니다. 프로젝트의 **현재 이미지와 환경변수**로 돕니다. 끝나지 않는 프로세스(큐 소비자·봇)는 배치 작업이 아니라 작업자 프로젝트로 띄웁니다(9.8) — 작업은 제한 시간에 끊기고, 새 이미지로 바꾸려면 직접 취소·재시작해야 합니다. ```bash # 시작 (202 {"ok":true,"id":"…"}) curl -fsS -X POST "$BASE/jobs" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"command":"python -m jobs.infer","completions":100,"parallelism":4,"size":"m","env":{"RUN_ID":"r1"}}' curl -fsS "$BASE/jobs" -H "$AUTH" # 최근 작업 목록 curl -fsS "$BASE/jobs/?logs=1&index=0" -H "$AUTH" # 상태 + 조각 0 의 로그 curl -fsS -X DELETE "$BASE/jobs/" -H "$AUTH" # 취소 ``` - 본문: `command`(비우면 이미지의 기본 명령), `completions`(조각 수 1~10000), `parallelism`(동시 실행 1~100), `size`, `gpu`, `gpuModel`, `env`, `timeoutHours`(기본 24, 최대 720), `retries`(기본 2), `name` - 모든 조각에 `JOB_COMPLETION_INDEX`(0부터)와 `JOB_COMPLETIONS` 환경변수가 들어옵니다. 명령이 0 으로 끝나면 그 조각은 완료입니다. - 결과를 돌려주는 통로는 없습니다. 작업이 직접 데이터베이스나 오브젝트 스토리지에 씁니다. - 본문의 `env` 는 작업 기록에 평문으로 남습니다. 비밀값은 프로젝트 환경변수에 둡니다. - 프로젝트당 동시에 20개까지이고, 조직 한도에 함께 계산됩니다. - 메모리 한도를 넘으면 앱 로그에는 아무것도 남지 않고 작업이 실패로 끝납니다. 작업 상태의 `message` 가 "메모리 한도를 넘어 종료되었습니다" 이면 `size` 를 올려 다시 시작합니다. ### 9.4 예약 호출 (크론) 사람이 프로젝트 설정 → “예약 호출”에 한 줄씩 `분 시 일 월 요일 경로` 를 적으면, 그 시각에 플랫폼이 앱의 그 경로를 GET 으로 부릅니다(예 `*/15 * * * * /api/cron/sync`). 에이전트는 핸들러를 만들고 넣을 줄을 사용자에게 알려 줍니다. - 시간대는 **한국 시간(Asia/Seoul)** 입니다. Vercel 의 `crons`(UTC)를 옮길 때는 시(hour)에 9를 더합니다. - `CRON_SECRET` 환경변수가 있으면 `Authorization: Bearer ` 헤더가 붙습니다(Vercel 과 같은 규약). - 응답이 2xx 가 아니거나 300초를 넘기면 실패입니다. 앞 회차가 돌고 있으면 다음 회차는 건너뜁니다. 프로젝트당 20개까지입니다. ### 9.5 데이터베이스와 파일 저장소 관리형 데이터베이스는 프로젝트와 별개의 리소스입니다. 연결하면 접속 정보가 그 프로젝트 환경변수에 들어가고 자동으로 재배포됩니다. 에이전트는 앱이 아래 환경변수 이름을 읽게 만들고 API 로 만듭니다. ```bash curl -fsS -X POST "https://deploy.core.today/api/v1/databases" -H "Authorization: Bearer $KUBECORE_AGENT_TOKEN" -H "Content-Type: application/json" \ -d '{"slug":"acme-shop-db","engine":"postgres18","size":"s","disk":10,"project":"acme-shop"}' # → 201 {"database":{"slug","engine","phase","ready":false,…},"attached":{"project":"acme-shop","env":"DATABASE_URL","redeployed":true}} ``` - 본문: `slug`(필수) · `engine`(기본 `postgres18`, 아래 표) · `name` · `size` · `disk` · `storage` · `project`(연결할 프로젝트) · `env`(넣을 환경변수 이름, 기본은 표의 첫 이름). 뜻은 아래 링크 파라미터 표와 같습니다. 자동 백업 일정은 엔진 기본값입니다. - `GET https://deploy.core.today/api/v1/databases/` 의 `ready` 가 `true` 가 될 때까지 기다립니다(보통 1~2분). 연결은 그 전에 해도 되고, 앱은 DB 가 뜰 때까지 접속 오류를 낼 수 있습니다. - 나중에 연결하거나 다른 이름으로 하나 더 넣으려면 `POST https://deploy.core.today/api/v1/databases//attach` `{"project":"","env":"<이름>"}`. - 응답에 접속 주소·비밀번호는 없습니다. 앱은 환경변수로만 받습니다. - 엔진 설정(LLM 키)이 필요한 엔진(메모리 계열)은 API 로 만들 수 없습니다(400) — 사용자에게 아래 링크를 줍니다. 사람이 만들 때는 **값이 채워진 생성 링크**를 줍니다: ``` https://deploy.core.today/databases/new?engine=postgres18&slug=acme-shop-db&name=Acme%20Shop%20DB&size=s&disk=10&project=acme-shop ``` | 파라미터 | 뜻 | |---|---| | `engine` | 엔진 (아래 표의 `engine` 값). 만든 뒤 바꿀 수 없음 | | `slug` | 데이터베이스 이름. 프로젝트 slug 와 같은 규칙이고 만든 뒤 못 바꿈. 보통 `<앱>-db` | | `name` | 표시 이름 (60자 이내) | | `size` | 크기: `xs` · `s` · `m` · `l` · `xl` · `xxl`. 기본 `s`. 엔진의 최소 크기보다 작으면 무시됨. 만든 뒤 바꿀 수 없음 | | `disk` | 디스크 GiB: `5` · `10` · `20` · `50` · `100` · `200`. 기본 `10`. 만든 뒤 키울 수 없음 | | `storage` | `replicated`(기본, 서버 장애에도 다른 서버에서 다시 뜸) 또는 `local` | | `project` | 연결할 프로젝트 slug. 만든 뒤 상세 화면의 “프로젝트에 연결”에 미리 골라짐(연결은 사람이 누름) | | `env` | 연결할 때 접속 주소를 넣을 환경변수 이름 (대문자·숫자·밑줄). 기본은 아래 표의 첫 이름. 앱이 다른 이름을 읽을 때만 씁니다 | | `engine` | 엔진 | 종류 | 연결하면 들어가는 환경변수 | 자동 백업 | |---|---|---|---|---| | `postgres18` | PostgreSQL 18 (+pgvector) | 관계형 (PostgreSQL 계열) | `DATABASE_URL` | 매일 | | `paradedb` | ParadeDB (PostgreSQL 17 + pg_search) | 관계형 (PostgreSQL 계열) | `DATABASE_URL` | 매일 | | `vectorchord` | VectorChord (PostgreSQL 18 + vchord) | 관계형 (PostgreSQL 계열) | `DATABASE_URL` | 매일 | | `postgis` | PostGIS (PostgreSQL 18 + PostGIS 3.6) | 관계형 (PostgreSQL 계열) | `DATABASE_URL` | 매일 | | `postgres` | PostgreSQL 17 (+pgvector) | 관계형 (PostgreSQL 계열) | `DATABASE_URL` | 매일 | | `mongodb` | MongoDB 8 — S 이상 | 문서 | `MONGODB_URI` | 없음 | | `redis` | Redis 8 | 키-값 · 캐시 | `REDIS_URL` | 매일 | | `valkey` | Valkey 8 | 키-값 · 캐시 | `REDIS_URL` | 매일 | | `qdrant` | Qdrant | 벡터 | `QDRANT_URL` `QDRANT_API_KEY` | 없음 | | `weaviate` | Weaviate | 벡터 | `WEAVIATE_URL` `WEAVIATE_API_KEY` | 없음 | | `lancedb` | LanceDB 저장소 | 벡터 | `LANCEDB_URI` `AWS_ACCESS_KEY_ID` `AWS_SECRET_ACCESS_KEY` `AWS_REGION` `AWS_ENDPOINT_URL` `AWS_ENDPOINT` `AWS_ALLOW_HTTP` | 매일 | | `opensearch` | OpenSearch 3 — L 이상 | 검색 · 분석 | `OPENSEARCH_URL` `OPENSEARCH_USERNAME` `OPENSEARCH_PASSWORD` | 없음 | | `meilisearch` | Meilisearch | 검색 · 분석 | `MEILISEARCH_URL` `MEILI_MASTER_KEY` | 없음 | | `clickhouse` | ClickHouse — M 이상 | 검색 · 분석 | `CLICKHOUSE_URL` `CLICKHOUSE_USER` `CLICKHOUSE_PASSWORD` `CLICKHOUSE_DB` | 없음 | | `neo4j` | Neo4j 5 (Community) — M 이상 | 그래프 | `NEO4J_URI` `NEO4J_USERNAME` `NEO4J_PASSWORD` | 없음 | | `falkordb` | FalkorDB | 그래프 | `FALKORDB_URL` | 매일 | | `graphiti` | Graphiti (시간 지식 그래프) — M 이상 | AI 메모리 | `GRAPHITI_URL` | 없음 | | `mem0` | Mem0 (에이전트 메모리) — S 이상 | AI 메모리 | `MEM0_API_URL` `MEM0_API_KEY` | 없음 | | `hindsight` | Hindsight (에이전트 메모리) — L 이상 | AI 메모리 | `HINDSIGHT_API_URL` | 없음 | | `s3` | 오브젝트 스토리지 (S3 호환) | 오브젝트 스토리지 | `AWS_ENDPOINT_URL_S3` `AWS_ACCESS_KEY_ID` `AWS_SECRET_ACCESS_KEY` `AWS_REGION` | 매일 | 링크로 만들 때의 순서: ① 데이터베이스 링크 → 사용자가 “데이터베이스 만들기” ② 프로젝트 링크(4.3) → “프로젝트 만들기” ③ 데이터베이스 상세 화면의 “프로젝트에 연결”(`project` 를 줬으면 미리 골라져 있음) → 재배포. ②의 첫 빌드는 접속 정보 없이 떠서 잠깐 `crashloop` 일 수 있고, ③의 재배포로 풀립니다. - 링크는 폼을 채우기만 하고 비밀번호는 플랫폼이 만듭니다. 링크에 비밀값을 넣지 않습니다. LLM 키가 필요한 엔진 설정은 사용자가 폼에서 직접 넣습니다. - 데이터베이스 삭제·백업 복원은 API 에 없습니다(콘솔). - **같은 조직의 프로젝트에서만 접속됩니다.** 인터넷이나 개발 PC 에서는 닿지 않습니다. 마이그레이션은 앱 시작 시나 배치 작업으로 돌립니다. - **접속 정보 없이 뜬 앱은 `crashloop` 가 됩니다.** API 로는 프로젝트를 만든 직후 `project` 를 주고 데이터베이스를 만들면 연결 재배포로 풀립니다. - **주소는 `http://` 이고 TLS 를 쓰지 않습니다.** 오브젝트 스토리지를 JS SDK 로 쓸 때는 `forcePathStyle: true` 가 필요합니다. - **엔진·크기·디스크는 만든 뒤 바꿀 수 없습니다.** PostgreSQL 확장(`pg_search`·`vchord`·`postgis`)이 필요한 앱은 그 확장이 든 엔진을 골라야 합니다. - 조직당 기본 50개까지 만들 수 있습니다. ### 9.6 서비스가 여러 개인 앱 (docker-compose 옮기기) compose 파일을 그대로 받지 않습니다. **프로젝트 하나가 컨테이너 하나**라서 서비스를 하나씩 옮깁니다. | compose 에 있던 것 | Deploy 에서는 | |---|---| | 화면·API·워커 서비스 | 서비스마다 프로젝트 하나 (`POST https://deploy.core.today/api/v1/projects` 를 서비스마다 한 번). 포트가 없는 워커는 `"port": 0`(9.8) | | postgres·redis·벡터 DB 같은 데이터 서비스 | 프로젝트가 아니라 관리형 데이터베이스 | | `volumes` (업로드 파일·데이터 폴더) | 앱의 파일 저장 방식을 S3 로 바꾸고 오브젝트 스토리지나 외부 S3 버킷 연결. 이미 사무실 NAS 에 있는 모델·데이터 폴더를 읽기만 한다면 사람이 프로젝트 설정 → “NAS 폴더”에서 붙임(관리자가 허용한 폴더만) | | 서비스 이름으로 부르기 (`http://api:8080`) | 상대 프로젝트 개요 화면의 “클러스터 내부” 주소. **포트는 항상 80** | | `depends_on` | 순서를 맞춰 주지 않음. 데이터베이스 → 백엔드 → 화면 순으로 만듦 | | `command`·`entrypoint` 덮어쓰기 | `FROM 이미지` + `CMD […]` 두 줄 Dockerfile 로 Git 빌드 | ### 9.7 코드 샌드박스 (대화형 AI 서비스가 사용자 코드를 돌릴 때) 배포한 앱과 별개로, 앱(대화형 AI 서비스의 백엔드)이 **사용자 대신 코드를 돌리는 격리된 실행 환경**입니다. 사람이 프로젝트의 “샌드박스” 탭에서 켜고(허용 도메인 — 처음엔 PyPI 가 들어 있음 — 과 인터넷 허용을 정함) 샌드박스 토큰(`kcs_…`)을 발급해 앱 환경변수(예 `KUBECORE_SANDBOX_TOKEN`)에 넣습니다. 프로젝트 API 토큰(`kc_…`)으로는 열리지 않고, 샌드박스 토큰으로는 빌드·배포 API 가 열리지 않습니다. ```bash SBX=https://deploy.core.today/api/v1/projects/ AUTH="Authorization: Bearer $KUBECORE_SANDBOX_TOKEN" SUBJECT="X-Sandbox-Subject: <최종 사용자 ID>" # 선택. 주면 그 사용자가 만든 것만 보이고 쓸 수 있다 # 단발 실행 — 새 환경에서 한 번 돌리고 결과만(최대 3600초). 85초 안에 끝나면 200, 아니면 202 → GET $SBX/sandbox-runs/?wait=60 curl -fsS -X POST "$SBX/sandbox-runs" -H "$AUTH" -H "$SUBJECT" -H "Idempotency-Key: <요청 ID>" -H "Content-Type: application/json" \ -d '{"language":"python","code":"print(1+1)","files":[{"path":"in.csv","content":"a,b\n1,2\n"}],"timeoutSeconds":60,"labels":{"conversation":"c-123"}}' # 세션 — 파일·프로세스가 남는다 curl -fsS -X POST "$SBX/sandboxes" -H "$AUTH" -H "$SUBJECT" -H "Idempotency-Key: <대화 ID>" -H "Content-Type: application/json" \ -d '{"size":"s","network":"allowlist","idleMinutes":30,"labels":{"conversation":"c-123"}}' curl -fsS -X POST "$SBX/sandboxes//exec" -H "$AUTH" -H "$SUBJECT" -H "Content-Type: application/json" -d '{"command":"ls -la","timeoutSeconds":30}' # 90초가 넘는 명령: 시작만 하고(202 {"execId":"x-…"}) 조회 — running 이면 지금까지의 출력 curl -fsS -X POST "$SBX/sandboxes//exec" -H "$AUTH" -H "$SUBJECT" -H "Content-Type: application/json" -d '{"command":"python3 big.py","timeoutSeconds":1800,"async":true}' curl -fsS "$SBX/sandboxes//exec/?wait=60" -H "$AUTH" -H "$SUBJECT" # DELETE 면 취소 # 파일 (스트리밍, 100MB 까지) · 디렉터리 통째로 tar.gz curl -fsS -X PUT "$SBX/sandboxes//files?path=data/in.csv" -H "$AUTH" -H "$SUBJECT" --data-binary @in.csv curl -fsS "$SBX/sandboxes//files?path=out.docx" -H "$AUTH" -H "$SUBJECT" -o out.docx curl -fsS "$SBX/sandboxes//files?path=.&list=1" -H "$AUTH" -H "$SUBJECT" curl -fsS -X PUT "$SBX/sandboxes//files/archive?path=inputs" -H "$AUTH" -H "$SUBJECT" --data-binary @inputs.tar.gz curl -fsS "$SBX/sandboxes//files/archive?path=outputs" -H "$AUTH" -H "$SUBJECT" -o outputs.tar.gz curl -fsS -X POST "$SBX/sandboxes//reset" -H "$AUTH" -H "$SUBJECT" # 프로세스 모두 정리, 파일은 남음 curl -fsS -X DELETE "$SBX/sandboxes/" -H "$AUTH" -H "$SUBJECT" # 목록·정리: ?label=k=v(여러 번 AND), ?all=1 은 subject 와 상관없이 전부. DELETE 는 label 필수 curl -fsS "$SBX/sandboxes?label=conversation=c-123&all=1" -H "$AUTH" curl -fsS -X DELETE "$SBX/sandboxes?label=conversation=c-123&all=1" -H "$AUTH" # subject 별 사용량(추정, ?range=1h|24h|7d) → subjectHash = sha256(subject) hex 앞 16자 curl -fsS "$SBX/sandbox-usage?range=24h" -H "$AUTH" ``` - 이미지: `python`(3.12 + pandas·numpy·scipy·scikit-learn·statsmodels·matplotlib·seaborn·plotly·polars·duckdb·pyarrow·networkx·sympy·python-docx·openpyxl·xlsxwriter·xlrd·python-pptx·pypdf·pypdfium2·pdfplumber·pdf2image·pikepdf·img2pdf·reportlab·python-hwpx·pyhwp·docxtpl·pillow·opencv(headless)·scikit-image·imageio·pytesseract·qrcode·soundfile·kiwipiepy·rapidfuzz·faker·beautifulsoup4·lxml·requests·httpx·aiohttp·pytest·ipython·uv), `node`(22 + pptxgenjs·docx·exceljs·sharp·marked·typescript(`tsc`)·tsx·puppeteer·playwright-core — 전역 설치라 `require` 로 부른다), `bash`, LibreOffice headless(`soffice --headless --convert-to pdf`), pandoc, poppler(`pdftotext`·`pdftoppm`), 시스템 한글 글꼴(나눔·Noto CJK·Paperlogy·Pretendard). 작업 디렉터리 `/workspace`, 쓰기는 `/workspace`·`/tmp` 만, 권한 없는 사용자. - 헤드리스 브라우저: Chromium(`$CHROME_PATH` = `/usr/bin/chromium`)과 Node `puppeteer`·`playwright-core`, Python `playwright` 가 들어 있다. 브라우저를 내려받지 않는다(`npx playwright install` 불필요). puppeteer 는 인자 없이 `require("puppeteer").launch()`, playwright 는 `chromium.launch({ executablePath: process.env.CHROME_PATH })`(Python `p.chromium.launch(executable_path=os.environ["CHROME_PATH"])`). `setContent`·스크린샷·PDF 는 네트워크 `none` 에서도 되고, 외부 페이지는 `allowlist`(등록한 도메인만 — 프록시 설정이 들어 있어 따로 줄 것 없음, 그 밖의 도메인은 `ERR_TUNNEL_CONNECTION_FAILED`)나 `internet` 에서 연다. 크기는 `m` 이상을 권한다(`s` 도 되지만 두 배쯤 느리다). 결과 파일은 세션의 files API 로 받는다. - 명령줄 도구: `ffmpeg`·`ffprobe`, ImageMagick 6(`convert`·`identify` — `magick` 명령은 없고 PDF 읽기는 막혀 있으니 `pdftoppm` 을 쓴다), ghostscript(`gs`), `qpdf`, tesseract OCR(`kor`·`eng` — 한 덩어리 글은 `--psm 6`), graphviz(`dot`), mermaid(`mmdc -i a.mmd -o a.png` — 위 Chromium 을 쓴다), `rg`·`fd`·`7z`·`bc`·`sqlite3`·`jq`·`git`. plotly 의 `write_image`(kaleido)는 없다 — `write_html(..., include_plotlyjs=True)` 로 만든 뒤 Chromium 으로 스크린샷·PDF 를 찍는다. 모델·데이터를 따로 내려받는 라이브러리(spacy 모델·nltk 데이터·torch)는 없다. - 글꼴: 나눔(나눔고딕·나눔명조 등)·Noto Sans/Serif CJK KR·Paperlogy(`1 Thin`~`9 Black` 9굵기)·Pretendard(Thin~Black 9굵기). 설치된 이름은 `fc-list : family` 로 본다. 대표 이름 `Paperlogy`·`Pretendard` 에 굵기를 붙여 고른다 — CSS·HTML(Chromium) 은 `font-family: Pretendard; font-weight: 600` 처럼 숫자로, pptx·docx(LibreOffice 변환·미리보기) 는 대표 이름 + 굵게(bold)로 보통·굵게를 쓰고 그 밖의 굵기는 `Paperlogy 8 ExtraBold`·`Pretendard SemiBold` 처럼 굵기까지 붙인 이름을 쓴다. matplotlib 기본은 `NanumGothic`(`rcParams["font.family"]` 로 바꾼다). reportlab 은 TrueType 만 읽으므로 Pretendard(OTF)가 아니라 Paperlogy·나눔 `.ttf`(`/usr/share/fonts/truetype/` 아래)를 등록한다. Windows·Mac·한컴 글꼴은 들어 있지 않고 비슷한 글꼴로 바꿔 그린다 — 맑은 고딕(`Malgun Gothic`)·굴림·돋움·Apple SD Gothic Neo·함초롬돋움 → Pretendard, 바탕·궁서·함초롬바탕 → 나눔명조(굵기는 유지, pptx·docx 변환과 HTML 모두). matplotlib·reportlab 은 이 대체를 하지 않아 없는 이름을 주면 한글이 네모로 나오니 설치된 이름을 직접 쓴다. PDF·이미지로 내보내면 글꼴이 박혀 그대로 보이지만, pptx·docx 파일을 받은 사람 PC에 그 글꼴이 없으면 Office 가 다른 글꼴로 바꿔 보여 준다(Paperlogy·Pretendard 는 SIL OFL 무료 글꼴이라 설치를 안내하면 된다). - 크기 `size`: `s` 500m CPU·512Mi·작업 공간 1Gi, `m` 1 CPU·2Gi·작업 공간 2Gi, `l` 2 CPU·4Gi·작업 공간 5Gi, `xl` 4 CPU·8Gi·작업 공간 10Gi. - 네트워크 `network`: `none`(기본, 완전 차단) · `allowlist`(프로젝트에 등록한 도메인만, https — `HTTPS_PROXY` 가 들어 있어 pip·npm·curl·git·requests·Node fetch 가 그대로 된다, DNS 는 막힘) · `internet`(프로젝트가 허용했을 때만, 공인 인터넷 전체). 사내망·플랫폼 내부·다른 프로젝트·데이터베이스는 어느 것이든 막힌다. - 시간: 세션 명령 동기 최대 90초(기본 30), `"async": true` 면 최대 3600초(세션 하나에 동시 20개), 단발 실행 최대 3600초. 넘기면 그 명령의 프로세스를 모두 끝내고 `exitCode` 124·`timedOut: true`. 명령이 끝나면 백그라운드 프로세스도 끝낸다(`"keepBackground": true` 면 남김). - 세션 수명: 마지막 사용 뒤 `idleMinutes`(기본 30, 최대 1440) 또는 만든 뒤 24시간. 비동기 명령이 도는 동안은 그 제한 시간만큼 연장된다. 단발 실행 결과는 1시간 보관. - `Idempotency-Key` 헤더(세션 생성·단발 실행·exec): 같은 키(같은 subject)의 재시도는 새로 만들거나 다시 돌리지 않고 처음 것을 돌려준다(`replayed: true`). 시간 초과 뒤 재시도에 쓴다. - `labels`(세션·단발 실행, 10개까지, 키 소문자·숫자·`-_.` 1~40자, 값 1~63자)로 대화 ID 등을 붙이고 목록·일괄 삭제에 쓴다. - 한도: 출력 stdout·stderr 각 256KB(넘으면 `truncated: true`), 파일 100MB(JSON base64 는 10MB), tar 받기 1GB, 단발 실행 입력 512KB, 명령마다 프로세스 256개, 동시에 세션 200개(subject 당 20개)·단발 실행 100개. - 오류 본문 `{ "error", "code" }`: `budget_exhausted`(402 — 조직 예산 소진, 사용자가 아니라 운영자에게 알린다) · `limit_sessions`·`limit_subject_sessions`·`limit_runs`·`limit_async`(409) · `network_not_allowed`·`sandbox_disabled`·`session_not_ready`·`session_lifetime`(409) · `file_too_large`·`input_too_large`(413) · `invalid_path`·`invalid_label`·`invalid_request`(400) · `not_found`(404, 다른 subject 의 것 포함) · `unauthorized`(401). - 토큰은 서비스 백엔드에만 둔다(브라우저·모바일 앱 금지). 모델에는 `run_code`·`sandbox_exec`·`sandbox_write_file`·`sandbox_read_file` 같은 도구를 주고 백엔드가 이 API 로 바꿔 보낸다. ### 9.8 작업자 — 포트 없이 계속 도는 프로세스 큐를 읽어 처리하는 소비자, 메시지 봇, 주기적으로 무언가를 하는 루프처럼 **요청을 받지 않고 계속 떠 있어야 하는 프로세스**는 `"port": 0` 으로 프로젝트를 만듭니다(콘솔에서는 종류 “작업자”). 빌드·환경변수·크기·복제본·GPU·NAS·로그·롤백은 웹 프로젝트와 같습니다. - 주소·헬스체크·도메인·브랜치 미리보기·예약 호출이 없습니다(`url` 은 `null`, `kind` 는 `"worker"`). `PORT` 환경변수도 들어가지 않습니다. - 새 버전은 **옛 Pod 를 내린 뒤** 뜹니다. 두 버전이 같은 큐·스키마를 동시에 만지지 않지만 그동안(보통 수십 초) 처리가 멈춥니다. 복제본이 2 이상이면 하나씩 바뀝니다. - 새 Pod 가 20초 넘게 떠 있어야 배포 성공입니다. 그 전에 끝나거나 죽으면 빌드가 실패로 끝나고 원인이 빌드 결과에 나옵니다. - 프로세스가 끝나면(종료 코드 0 이어도) 다시 시작되고, 반복되면 “반복 종료” 상태가 됩니다. 일을 마치고 끝나야 하는 실행은 배치 작업(9.3)을 씁니다. - 종료 신호(SIGTERM)를 받으면 하던 일을 정리하고 30초 안에 끝냅니다. 큐에서 꺼낸 일은 끝까지 처리하거나 되돌려 놓아야 다음 Pod 가 이어받습니다. - 웹 서비스↔작업자는 만든 뒤 바꿀 수 없습니다. 바꾸려면 새 프로젝트를 만듭니다. - **웹 서비스와 같은 저장소의 작업자**는 같은 `repo` 로 프로젝트를 하나 더 만들고 `startCommand` 에 작업자 명령을 줍니다(예: `python -m app.worker`). Dockerfile 빌드면 이 명령이 Dockerfile 의 `CMD` 를 대신하고(`ENTRYPOINT` 는 그대로), 자동 감지 빌드면 시작 명령이 됩니다. push 마다 둘 다 다시 빌드되게 하려면 사람이 두 프로젝트의 웹훅 URL 을 모두 저장소에 등록합니다(9.2) — 하나만 등록하면 다른 쪽은 옛 코드로 남습니다. ## 10. 제한사항 - 프로젝트에 영구 볼륨이 없습니다(데이터는 관리형 데이터베이스·오브젝트 스토리지). 예외로 관리자가 조직에 허용한 사무실 NAS 폴더를 사내(@core.today) 계정만 사는 조직에서 사람이 설정에서 붙일 수 있고(기본 읽기 전용, 컨테이너 경로는 기본 `/data/<폴더 이름>`), 앱은 그 경로를 일반 폴더처럼 읽습니다. API 로는 붙일 수 없습니다. 쓰기로 붙인 폴더는 NAS 가 앱의 실행 사용자 번호(uid)로 권한을 따지므로, 비루트 `USER` 이미지라면 그 폴더가 그 uid 에게 쓰기 가능해야 합니다 — Dockerfile 의 `chown` 은 NAS 에 닿지 않으니 사용자가 관리자에게 요청합니다. - 이미지 레지스트리로 직접 push 할 수 없습니다. Git 저장소에서 빌드하거나 공개 레지스트리(Docker Hub·GHCR)의 이미지를 씁니다. 비공개 외부 레지스트리는 지원하지 않습니다. - 컨테이너 인자·커맨드를 콘솔에서 바꿀 수 없습니다. - 공개 주소가 없는 내부 전용 프로젝트를 만들 수 없습니다. - TCP/UDP 포트 공개, 100 MB 가 넘는 요청, ARM 이미지를 지원하지 않습니다. - 커스텀 도메인은 사람이 프로젝트 설정의 “도메인”에서 호스트명을 추가한 뒤(화면에 걸어야 할 레코드가 나옴) 그 도메인의 DNS 에 `CNAME → cname.core.today` 을 추가하면 열립니다(추가 먼저 — DNS 를 먼저 바꾸면 그 사이 404). https 인증서는 자동입니다. 플랫폼 주소(`<이름>.apps.core.today`, 프로젝트당 몇 개)와 미리보기 주소는 바로 열립니다. - 주소는 프로젝트에 붙였다 뗄 수 있는 이름입니다. 사람이 콘솔에서 플랫폼 주소를 더하거나 빼고, 기본 주소(`url`)를 커스텀 도메인으로 바꿀 수 있습니다. 뺀 주소는 30일 동안 보류되어 다른 조직이 못 쓰고(그동안 들어온 요청은 기본 주소로 넘기거나 안내), 그 뒤에는 누구나 쓸 수 있습니다. 앱 설정·OAuth 리디렉트에 주소를 넣을 때는 `url` 을 기준으로 하고, 주소가 바뀌면 사용자에게 함께 바꾸라고 알립니다. - 프로젝트·데이터베이스 삭제, 커스텀 도메인, NAS 폴더, 알림은 API 에 없습니다(콘솔에서 사람이 합니다). ## 11. 요금과 예산 - 실제로 쓴 CPU·메모리만큼 조직 예산(크레딧)에서 차감됩니다. 복제본이 0 이면 요금이 나가지 않습니다. - 예산이 없거나 소진된 조직은 새 프로젝트·빌드 시작·증설이 막힙니다(빌드 API 는 402). 돌던 서비스는 계속 돕니다. 충전은 사용자가 플랫폼 관리자에게 요청합니다. - 단가표: https://deploy.core.today/docs#billing