axhub.yaml
ax-hub-backend deploy manifest domain 기준 canonical manifest schema
axhub.yaml은 저장소에 커밋하는 배포 계약 파일이에요. 템플릿으로 만든 앱은 이 파일이 이미 맞춰져 있어서 보통 손댈 일이 없어요.
빌드 명령을 직접 고정하거나, 앱 전용 데이터베이스·스토리지를 선언하거나, compose 진입 서비스를 지정할 때 이 페이지에서 정확한 필드를 찾아보세요. 파일이 없어도 배포는 돼요. AxHub가 저장소를 보고 빌드 방법을 알아서 정하고, 그 순서도 이 페이지에 있어요. 각 기능을 실제로 붙이는 절차는 SDK 설치하기부터 이어지는 기능 가이드에 있어요.
최소 예시
version: axhub/v1
name: hello-axhub
root: web # 앱이 하위 폴더에 있을 때만 (monorepo)
runtime:
port: 3000
health_path: /
build:
strategy: auto
framework: node
deploy_method: docker
dockerfile: Dockerfile
database:
engine: postgres
storage:
enabled: true
env:
required:
- name: OPENAI_API_KEY
scope: runtime
optional:
- name: NEXT_PUBLIC_API_URL
scope: build
ci:
commands:
- npm test
timeout: 300모든 field는 optional이에요. 비어 있는 값은 DB 설정 또는 resolver 기본값으로 떨어져요.
root — 앱이 하위 폴더에 있을 때
한 저장소 안에 여러 프로젝트가 있고 실제 배포 대상이 하위 폴더(예: web/)라면, 저장소 최상위 axhub.yaml에 한 줄을 적어요.
root: web이 한 줄이면 그 폴더가 앱의 최상위인 것처럼 동작해요. 우회용 Dockerfile을 최상위에 두고 버티던 걸 걷어내고, 자동 감지와 Dockerfile 자동 생성을 그대로 받을 수 있어요.
무엇이 root 기준이 되나요
- Dockerfile · compose 파일 · 프레임워크 마커(
package.json등) 감지 - Dockerfile 자동 생성과 빌드 컨텍스트(복사 범위 ·
.dockerignore) - 이 파일의 다른 경로 필드 —
build.dockerfile·build.compose_file·build.static_output_dir·metadata.icon - CI 명령과 정적 사이트 빌드가 실행되는 위치
즉 root: web + dockerfile: docker/prod.Dockerfile이면 실제로는 web/docker/prod.Dockerfile을 봐요. "이건 저장소 기준, 저건 앱 기준"을 외울 필요가 없어요.
규칙
| 쓴 값 | 결과 |
|---|---|
web · ./web · web/ | 전부 web으로 같게 봐요 |
web/app | 두 단계 이상 깊어도 돼요 |
. · ./ | 미선언과 같아요 (저장소 최상위) |
/web · ../other | 배포 전에 실패해요 — 저장소 안의 상대 경로만 돼요 |
| 그 폴더에 파일이 하나도 없음 | 배포 전에 실패해요 |
설정은 저장소 최상위 axhub.yaml에서만 읽어요. web/axhub.yaml이 같이 있으면 무시하고 배포 결과에 경고 한 줄을 남겨요. 반대로 최상위에 파일이 없고 web/axhub.yaml만 있으면 root 선언이 없는 것으로 봐요.
root를 선언하면 저장소 최상위의 Dockerfile은 무시돼요 — root가 곧 앱의 경계예요- 콘솔이 아니라 저장소 설정이라, push 한 번으로 적용돼요 (재연결 · 콘솔 조작 없음)
- 보안 스캔은 그대로 저장소 전체를 봐요 (범위를 좁히지 않아요)
root를 선언하지 않은 앱은 이 기능이 생기기 전과 완전히 똑같이 동작해요
파일과 우선순위
| 항목 | 현재 계약 |
|---|---|
| canonical filename | axhub.yaml |
| max size | 8 KiB |
| priority | manifest > DB AppSpecData > resolver default |
| deploy resolution | manifest → Dockerfile → Compose → Railpack auto-detection |
빌드 방법이 정해지는 순서
AxHub는 저장소 루트에 어떤 파일이 있는지를 보고 빌드 방법을 정해요. 아래 순서대로 위에서부터 검사하고, 하나라도 맞으면 거기서 멈춰요.
각 단계에서 정확히 무엇을 보는지는 이래요.
axhub.yaml— 있으면 이 manifest를 우선 적용해요.build.dockerfile지정,strategy: pinned+start고정 실행이 이 단계에서 반영돼요.Dockerfile— 루트에 있으면 단일 이미지로 빌드해요.- Compose 파일 —
docker-compose.yml·docker-compose.yaml·compose.yaml·compose.yml순서로 찾아요. - Railpack 자동 감지 — 위가 전부 없으면 마커 파일로 프레임워크를 추론해요:
package.json→node ·go.mod→go ·requirements.txt/pyproject.toml→python ·Gemfile→ruby ·pom.xml/Gradle 파일→java·kotlin ·Cargo.toml→rust.- 자동으로 만든 Dockerfile 은 Node · Python · Go · Ruby · Java 밖의 언어(Rust 등)에선 툴체인이 없는 이미지로 만들어져 빌드가 실패할 수 있어요. 이런 앱은 Dockerfile 을 직접 둬요.
넷 다 없으면 빌드할 방법을 찾지 못해 배포가 실패해요. 빌드·실행 명령이 애매하거나 짐작에 맡기지 않고 직접 고정하려면 axhub.yaml을 저장소에 두면 돼요 — 최소 예시에서 시작하세요.
최상위 필드
| 필드 | 타입 | 설명 |
|---|---|---|
version | string | 현재 문서화 값은 axhub/v1. unknown 값은 forward-compatible하게 받아들일 수 있지만 known fields 검증은 유지돼요. |
name | string | 표시용 label. 저장된 앱 이름과 강제 동기화하지 않아요. |
root | string | 앱이 하위 폴더에 있을 때 그 폴더를 앱의 최상위로 삼아요 (monorepo). |
metadata | object | 앱 이름 · 설명 · 아이콘을 저장소에 두고 코드와 함께 옮겨요. |
runtime | object | port · health probe path · 헬스체크 대기 시간(healthcheck) · compose 진입 서비스(entry_service). |
build | object | build/deploy method와 command hints. |
database | object | 앱 전용 DB 요청 — engine: postgres. 필요하면 extensions로 Postgres 확장, roles로 추가 DB 계정까지. |
storage | object | 앱 전용 파일 스토리지 요청 — enabled: true. |
env | object | 필요한 env 이름과 scope. 값은 절대 manifest에 저장하지 않아요. |
ci | object | build 전 CI command와 timeout. |
runtime
| 필드 | 타입 | 제한 |
|---|---|---|
port | integer | 1~65535. 비어 있으면 Dockerfile EXPOSE 또는 adapter 기본값. compose 모드에서는 무시돼요 — 포트는 compose 파일이 결정해요. |
entry_service | string | compose 전용. 바깥 주소에 연결할 서비스 이름. docker · static 모드에서는 무시돼요. 자세한 규칙은 아래 compose 진입 서비스를 보세요. |
health_path | string | liveness/readiness probe path. 비어 있으면 / fallback. |
healthcheck | object | docker 전용. 앱이 처음 뜰 때까지 기다리는 헬스체크 대기 시간을 정해요. compose · static 모드에서는 무시돼요. 자세한 규칙은 아래 헬스체크 대기 시간을 보세요. |
replicas | map | compose 전용. 서비스 이름 → 파드 수. 값은 1 이상이고, 안 적은 서비스는 플랫폼 기본값을 따라요. |
cache | object | docker 전용. 재시작·재배포 뒤에도 남는 캐시 볼륨. size(1Gipaths(절대경로 1 |
ephemeral_storage | string | 컨테이너 임시 스토리지 상한. 2Gi·1536Mi 처럼 Mi/Gi 단위, 1Gi 이상 10Gi 이하. 비어 있으면 플랫폼 기본 1Gi. compose 모드는 모든 서비스에 같은 값, static 모드는 무시돼요. 자세한 규칙은 아래 임시 스토리지를 보세요. |
레플리카를 compose 파일의 deploy.replicas가 아니라 여기에 두는 이유가 있어요. compose 파일은 로컬에서 docker compose up으로 그대로 돌릴 수 있게 순수하게 두고, "어떻게 배포할지"는 매니페스트가 갖는 거예요.
runtime:
replicas:
web: 2
beat: 1 # 크론은 하나만 — 두 개면 매번 두 번 발화해요
redis: 1스케줄러와 공유 저장소는 1로 못 박으세요. 서비스마다 지정하지 않으면 전부 같은 수로 뜨는데, 크론 서비스가 2개가 되면 작업이 두 번 실행돼요.
캐시 볼륨 (cache)
컨테이너 파일시스템은 파드가 죽으면 사라져요. 기동할 때 모델·패키지를 받는 앱은 재시작·재배포마다 같은 파일을 다시 받게 되는데, 그 경로를 캐시 볼륨에 두면 한 번 받은 파일이 남아요.
runtime:
cache:
size: 5Gi # 1Gi ~ 50Gi
paths:
- /root/.cache/huggingface
- /root/.cache/pip플랫폼이 운영·스테이징 환경마다 디스크 하나를 만들고 적어 둔 경로들을 거기에 연결해요. 이미지가 non-root 사용자(USER app 등)로 돌아도 그 경로에 쓸 수 있게 열어 둬요. 경로는 서로 겹치면 안 돼요(/a 와 /a/b 동시 지정 불가). size 는 2Gi·1536Mi 처럼 Mi/Gi 단위의 정수로 적어요. 디스크는 그 환경에 cache 가 처음 적용될 때 만들어지고 크기도 그때 정해져요 — 나중에 size 를 바꿔도 기존 디스크가 그대로 쓰여요. 대개는 그 환경에서 cache 를 처음 선언한 배포가 그때예요. 스테이징은 켜는 순간 운영 중인 버전으로 채워지므로, 그 버전이 cache 를 쓰고 있으면 스테이징 디스크는 켤 때 그 버전의 size 로 만들어져요. 그 뒤 스테이징 배포에서 size 를 바꿔도 이 디스크가 그대로 쓰여요.
파드가 하나인 앱에서만 쓸 수 있어요. 디스크 하나에 파드 하나만 붙기 때문에 레플리카가 2 이상이면 배포가 거절돼요. 앱을 일시정지하면 디스크도 지워져요(스테이징이 꺼지면 스테이징 디스크가 지워져요). 지워진 디스크는 그 환경에 cache 가 다시 적용될 때(다음 배포·재개, 스테이징은 다시 켤 때) 새로 만들어지고 크기도 그때 다시 정해져요 — 캐시는 "다시 받으면 되는" 파일에만 쓰세요. compose 앱은 compose 파일의 named volume 이 같은 역할이에요.
저장소를 고칠 수 없으면 같은 값을 앱 설정으로도 둘 수 있어요 — PUT /api/v1/apps/{appID}/runtime-overrides 에 cache·ephemeral_storage·port·health_path·healthcheck 를 보내면 다음 배포·재개부터 적용돼요. 같은 주소를 GET 하면 마지막 운영 배포가 실제로 쓴 값과 그중 앱 설정에서 온 항목(override_fields)을 알려 줘요. 앱 설정은 저장소가 정하지 않은 자리만 채워요 — axhub.yaml 에 선언이 있으면 yaml 이 우선이고, 포트는 Dockerfile 의 숫자 EXPOSE 가 있으면 그쪽이 우선해요.
임시 스토리지 (ephemeral_storage)
컨테이너가 자기 파일시스템(/tmp, 작업 폴더 등)에 쓸 수 있는 크기예요. 기본은 1Gi이고, 넘기면 플랫폼이 파드를 축출(Evicted) 하고 새로 띄워요. 기동할 때 모델·패키지·데이터를 수 GB 받아 두는 앱은 이 한도에 걸려 "받다가 축출되고 다시 받는" 루프를 돌아요. 그런 앱만 올리세요.
runtime:
ephemeral_storage: 4Gi # 1Gi ~ 10Gi, Mi/Gi 단위임시 스토리지는 파드가 죽으면 사라져요. 재시작 뒤에도 남아야 하는 파일(모델 캐시 등)은 이미지에 굽거나 storage(앱 전용 파일 스토리지)에 두세요.
헬스체크 대기 시간 (healthcheck)
배포하면 AxHub는 새 컨테이너가 처음 헬스체크에 응답할 때까지 일정 시간을 기다려요. 이 시간이 헬스체크 대기 시간이에요. 이 시간 안에 응답하지 못하면 컨테이너를 재시작해요. 재시작 뒤에도 뜨지 못하면 배포가 실패하는데, 재시작이 파드 이벤트로 확인되면 healthcheck.startup_budget_exceeded, 아니면 healthcheck.container_crash로 실패해요. 재시작 뒤에 응답하면 경고(startup_budget_restart)만 남기고 배포는 성공할 수 있어요.
헬스체크 대기 시간 = start_period + interval × retries (60초 ~ 5분)아무것도 적지 않으면 60초예요. 마이그레이션 · 시드 · 워밍업 때문에 60초 안에 못 뜨는 앱은 이렇게 적어요. 뜻은 Dockerfile의 HEALTHCHECK, compose의 healthcheck와 같아요.
runtime:
port: 3000
health_path: /health # 검사가 치는 경로
healthcheck:
start_period: 90s
interval: 10s
timeout: 5s
retries: 6 # → 대기 시간 90 + 10×6 = 150초| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
start_period | duration | 0s | 이 시간 동안은 실패를 세지 않아요 |
interval | duration | 3s | 검사 간격 |
timeout | duration | 5s | 검사 1회의 응답 제한. 5초보다 작게 적어도 5초로 올려요 — 부팅 중에는 CPU가 부족해 검사 자체가 느려요 |
retries | integer | 20 | 연속 실패 허용 횟수 |
규칙
- 값은 단위가 붙은 시간(
90s,2m)만 받아요.90처럼 단위가 없거나 음수면 배포 전에 거절돼요. - 계산 결과가 60초보다 짧으면 60초로, 5분보다 길면 5분으로 맞춰요. 상한을 넘겨도 거절하지는 않아요.
- 적은 값만 바뀌고 나머지는 기본값으로 채워요.
start_period: 60s만 적으면 60 + 3×20 = 120초예요. - docker 전용이에요. compose 앱은 compose 파일의
healthcheck가 정본이고 이 블록은 무시돼요 (test가 없으면 포트 검사 · 60초,NONE이거나disable: true면 검사하지 않아요). 정적 앱도 무시돼요. - Dockerfile의
HEALTHCHECK명령은 읽지 않아요 — 같은 값을 여기에 적어요. - 준비(readiness) · 살아있음(liveness) 검사는 이 값과 무관해요. 두 검사는
health_path만 써요.
부팅에 마이그레이션 · 워밍업이 있으면 그 시간까지 더해서 적으세요. 스테이징도 운영과 같은 사양으로 돌기 때문에 뜨는 시간이 두 환경에서 비슷해요 — 스테이징에서 잰 시간을 그대로 기준으로 잡아도 돼요.
- 스테이징 · 운영 · 운영 반영(promote) · 재개(resume) 모두 같은 대기 시간을 써요. 배포 시점의
health_path·healthcheck가 배포 기록에 함께 남아서, 운영 반영이 코드를 다시 읽지 않아도 같은 값을 써요. - 대기 시간 초과 · 크래시 등 실패 사유 코드별 조치는 헬스체크 실패 사유 코드에 있어요.
build
| 필드 | 값 | 설명 |
|---|---|---|
strategy | auto | pinned | 기본 auto. pinned + start면 generated Dockerfile path로 고정 실행 가능. |
framework | node | python | go | ruby | java | kotlin | rust | unknown 등 | CI/image selection hint. |
install | string | framework preset용 install command. |
build | string | framework preset용 build command. |
start | string | runtime start command. |
dockerfile | path | 기본 Dockerfile 대신 사용할 Dockerfile path. |
deploy_method | docker | compose | static | Docker 단일 이미지, Compose multi-service, 또는 정적 사이트. |
compose_file | path | compose mode에서 사용할 compose file. 기본 docker-compose.yml. |
static_output_dir | path | static mode에서 배포할 빌드 산출물 디렉터리 (앱 루트 기준). 기본 dist. .이면 앱 루트 자체를 배포해요 (빌드 없는 HTML 사이트). |
플러그인 import로 정적 앱을 올릴 때 내 컴퓨터에서 도는 빌드는 install · build 값을 쓰지 않아요. 잠금 파일로 고른 패키지 매니저로 의존성을 설치하고 package.json의 build 스크립트를 실행해요 (pnpm-lock.yaml→pnpm · bun.lock 또는 bun.lockb→bun · yarn.lock→yarn · 그 밖에는 npm).
작업공간이 Axhub 저장소(self-hosted Git)를 쓰면 정적 앱도 소스를 앱 저장소에 push하고, push로 시작된 배포가 서버에서 빌드해요. 이때는 install · build 값을 쓰고, 비어 있으면 잠금 파일로 의존성을 설치(package-lock.json→npm ci · pnpm-lock.yaml→pnpm · yarn.lock→yarn · 그 밖에 package.json만 있으면 npm install)한 뒤 package.json이 있을 때 npm run build를 실행해요. 그다음 static_output_dir 폴더를 배포해요. .이면 .git · node_modules를 뺀 앱 루트 전체를 배포해요.
deploy_method는 앱 DB에서도 immutable이에요. manifest 값이 앱의 DB 값과 다르면 배포가 실패하니, 바꾸려면 보통 새 앱으로 재생성해야 해요. axhub.yaml이 없어도 같아요 — compose 파일만 있으면 compose, 자동 감지면 docker로 판정되는데, 이 값이 앱 값과 다르면 같은 실패가 나요.
compose 진입 서비스
여러 서비스로 이루어진 compose 앱에서 바깥 주소({앱}.{테넌트}.…)에 연결되는 서비스는 하나뿐이에요. 명시하지 않으면 compose 파일에서 호스트 포트를 명시한 첫 번째 서비스가 진입점이 돼요.
순서에 기대지 않으려면 axhub.yaml에 직접 적어요(권장):
runtime:
entry_service: backend # compose 전용 — 이 서비스가 바깥 주소에 연결돼요- 선언이 있으면 호스트 포트를 안 적은 서비스도 진입점이 될 수 있어요 —
ports: [{target: 8080}]이든expose: ["8080"]이든 그 서비스가 열리고, 포트는 선언된 것 중 첫 번째를 써요. - 선언한 이름이 compose 파일에 없으면 배포가 실패해요 (
build.entry_service_not_found— 실패 메시지에 실제 서비스 목록이 함께 나와요). - 선언한 서비스에 포트가 하나도 없으면 역시 실패해요 (
build.entry_service_has_no_port). - 미선언이면 기존 순서 규칙이 그대로 적용돼요.
- 진입 서비스는 웹훅을 받는 서비스이기도 해요. 바깥 주소와 같은 서비스, 같은 포트로 전달돼요.
포트 작성법별 실제 동작
"진입 후보"가 아니어도 내부 통신 포트는 살아 있어요 — 둘은 별개의 축이에요. 서비스끼리는 서비스 이름으로 서로를 불러요(http://backend:8080).
| 작성법 | 진입 후보 | 내부 통신 포트 |
|---|---|---|
ports: ["3000:3000"] | ✅ 3000 | 3000 |
ports: [{target: 7000, published: "7000"}] | ✅ 7000 | 7000 |
ports: ["4000"] (호스트 포트 없는 short syntax) | ❌ | 4000 |
ports: [{target: 6000}] (published 없음) | ❌ | 6000 |
expose: ["5000"] | ❌ | 5000 |
| 포트 선언 없음 | ❌ | ❌ 없음 |
포트를 하나도 안 적은 서비스는 형제 서비스가 이름으로 부를 수 없어요. 로컬 docker compose는 선언 없이도 서로 도달하니 로컬에서만 멀쩡한 상태가 되기 쉬워요 — 이 경우 배포 결과에 경고가 남아요. 큐 워커처럼 서버가 아니면 무시해도 돼요.
expose:는 내부 전용 포트로 흡수돼요 — 내부 통신은 열리고 진입점은 되지 않아요. 범위 표기(3000-3005) · udp는 지원하지 않고 경고와 함께 무시돼요.
진입점을 바꾸려면 호스트 포트 명시를 옮기거나 entry_service 선언을 바꾸고 다시 배포하면 한 번에 반영돼요. 주소 · 인증서 · 도메인 설정은 앱 단위라 그대로 유지돼요.
배포 결과에 뜨는 경고
경고는 배포를 막지 않아요 — 의도한 구성이면 무시해도 돼요.
| 경고 | 뜻 | 의도한 것이면 |
|---|---|---|
| 포트를 연 서비스가 여러 개 | 그중 하나만 바깥에 연결돼요 | 나머지를 target: · expose:로 바꿔 의도를 분명히 |
| 서비스를 이름으로 부를 수 없어요 | 포트를 하나도 안 적었어요 | 워커 · 배치면 무시해도 돼요 |
| 이 앱은 주소로 열리지 않아요 | 호스트 포트를 연 서비스가 없어요 | 배치 · 워커 전용 앱이면 정상이에요 |
| expose 항목을 읽을 수 없어요 | 범위 · udp는 미지원이에요 | 포트 번호 하나씩 나눠 적어요 |
| 선언에 따라 서비스가 연결돼요 | entry_service가 이겼고, 포트를 연 다른 서비스가 있어요 | 의도한 것이면 무시 |
metadata
앱 이름 · 설명 · 아이콘을 저장소에 적어 두면 코드와 함께 옮겨 다녀요. 콘솔에서 따로 채우지 않아도 돼요.
metadata:
name: 사내 위키
description: 팀 문서를 한곳에서 찾는 앱
icon: assets/icon.png
icon_dark: assets/icon-dark.png| 필드 | 설명 |
|---|---|
name | 앱의 실제 이름. 최상위 name(표시용 label)과 다른 자리예요 |
description | 스토어 · 목록에 나오는 설명 |
icon · icon_dark | 저장소 안의 이미지 경로. root를 선언했으면 그 폴더 기준이에요 |
- 전부 선택 항목이고, 운영에 실제로 반영될 때 적용돼요 — 운영에 바로 배포하거나, 스테이징 빌드를 운영에 반영(promote)할 때예요
- 값을 비워 두는 것은 "건드리지 말라"는 뜻이에요. 빈 문자열로 기존 값을 지울 수는 없어요
database
앱 전용 Postgres DB를 manifest로 요청해요. 선언한 채 배포하면 앱 전용 DB가 만들어지고, 런타임에 DATABASE_URL(쿼리용) · DIRECT_DATABASE_URL(마이그레이션용)이 자동 주입돼요.
database:
engine: postgres
extensions: [pgvector, pg_trgm] # 선택
roles: [runtime, auth] # 선택
owner_bypass_rls: true # 선택| 필드 | 값 | 설명 |
|---|---|---|
engine | postgres | 현재 지원하는 유일한 엔진. 앱 전용 DB를 켜는 방법은 이 선언 하나예요. |
extensions | 문자열 목록 | 배포 때 앱 DB에 미리 설치할 Postgres 확장. engine과 함께 적어야 하고, 아래 목록의 이름만 받아요. |
roles | 문자열 목록 | 기존 계정 옆에 더 만들 로그인 계정(최대 3개). 계정마다 DATABASE_URL_<이름> · DIRECT_DATABASE_URL_<이름>이 주입돼요. 이름은 영어 소문자로 시작하는 소문자 · 숫자 16자 이내이고 ro · staging은 쓸 수 없어요. engine과 함께 적어요. |
owner_bypass_rls | true · false | true면 기존 계정(표 주인)이 RLS를 건너뛰어요(BYPASSRLS). 표에 FORCE ROW LEVEL SECURITY를 걸 때 마이그레이션 · 백업이 막히지 않게 해요. 기본값 false. |
선언할 수 있는 확장은 13개예요 — pgvector(vector로 적어도 같아요) · pg_trgm · citext · unaccent · fuzzystrmatch · uuid-ossp · pgcrypto · hstore · ltree · intarray · tablefunc · btree_gin · btree_gist. 대소문자는 구분하지 않고 중복은 하나로 접혀요. 버전은 DB 인스턴스가 제공하는 기본 버전이 설치돼요.
- production과 staging은 서로 다른 DB를 받아요 — 각 환경에 배포할 때 그 환경의 DB가 만들어져요. 스테이징을 쓰는 앱은 운영에 반영(promote)할 때 운영 DB가 만들어지고, 이때는 심사한 스테이징 버전의 선언을 따라요. 확장도 각 환경의 DB에 따로 설치돼요.
- 확장은 앱이 새 버전으로 뜨기 전에 설치돼요. 이미 설치돼 있으면 그대로 두고, 설치에 실패하면 배포도
prepare.raw_db_extension_failed로 실패해요 — "배포는 됐는데 확장이 없는" 상태는 생기지 않아요. - 선언을 지워도 DB와 주입은 유지돼요. 끄기는 CLI(
apps raw-db disable)로만 해요. 확장도 선언에서 지운다고 회수되지 않아요 — 그 확장을 쓰는 표와 데이터가 그대로 남아요. - 정적 호스팅 앱(
deploy_method: static)이 선언하면 배포가prepare.database_unsupported_for_static으로 실패해요. extensions에 목록 밖 이름을 적거나engine없이extensions만 적으면 manifest 검증에서 배포가 시작 단계에 거절돼요. 사유에 문제 항목과 가능한 값이 함께 보여요.roles로 만든 계정은 접속만 되고 표 권한은 없어요 — 어떤 표를 볼지는 앱 마이그레이션이GRANT로 정해요. RLS를 받는 일반 계정이고, 동시에 5개까지 접속해요. 선언에서 빼면 그 계정은 로그인만 막히고 받았던 권한은 남아요. 계정 만들기에 실패하면 배포가prepare.raw_db_role_failed로 실패해요.owner_bypass_rls를 빼면 다음 배포에서 BYPASSRLS가 바로 회수돼요. 추가 계정에는 이 권한이 붙지 않아요.- 사용법은 데이터베이스 연결하기를, 계정 나누기는 DB 계정을 나눠 쓸 때를 보세요.
storage
앱 전용 S3 호환 파일 스토리지를 manifest로 요청해요. 선언한 채 배포하면 앱 전용 버킷이 만들어지고, 런타임에 STORAGE_ENDPOINT · STORAGE_BUCKET · STORAGE_ACCESS_KEY · STORAGE_SECRET_KEY가 자동 주입돼요.
storage:
enabled: true| 필드 | 값 | 설명 |
|---|---|---|
enabled | boolean | true면 다음 배포에서 프로비저닝. |
cdn | boolean | true면 버킷 파일을 서명 주소 CDN 으로도 내보내고 STORAGE_CDN_BASE_URL · STORAGE_CDN_SIGN_KEY 를 더 넣어 줘요. enabled: true 와 함께 적어야 해요. |
이 두 필드 밖의 키는 배포 전에 거부돼요.
- 버킷은 앱당 1개이고 production · staging이 공유해요.
- 선언을 지워도 버킷 · 파일 · 주입은 유지돼요. 앱 영구 삭제 시에만 함께 삭제돼요.
- 정적 호스팅 앱이 선언하면 배포가
prepare.storage_unsupported_for_static으로 실패해요. - 사용법 · 콘솔 API · 제한은 스토리지 연결하기를 보세요.
env
Manifest에는 env 이름과 scope만 적어요. 값은 axhub env set/update로 저장해요.
env:
required:
- name: OPENAI_API_KEY
scope: runtime
- name: NEXT_PUBLIC_API_URL
scope: build
optional:
- name: SENTRY_DSN
scope: both| scope | 주입 위치 |
|---|---|
build | build arg로만 전달 |
runtime | pod env로만 전달 |
both | build와 runtime 양쪽에 필요 |
scope를 생략하면 기본값은 runtime이에요. 이름은 비어 있거나 줄바꿈/= 문자를 포함할 수 없어요.
AxHub 가 넣어 주는 이름(DATABASE_URL · DIRECT_DATABASE_URL · STORAGE_* · PORT 등)은 required 에 적지 않아요. 실행 중에 AxHub 가 넣어 주는 값이라 따로 저장하지 않아도 되고, 빌드에는 들어가지 않아요 — 빌드에 필요하면 scope: build 로 직접 저장해요.
ci
| 필드 | 제한 |
|---|---|
commands | 최대 10개 |
timeout | 1~600초 (검증 범위) |
CI command는 build 전에 실행되고, 실패하면 deploy가 멈춰요.
timeout 값은 현재 검증만 되고 실행 예산에는 반영되지 않아요 — 파이프라인의 CI 단계 예산은 10분으로 고정돼 있어요.