AXHub에 맞게 앱 만들기

AXHub 서버에서 앱이 실제로 뜨려면 지켜야 하는 조건과, 어기면 나는 배포 실패를 규칙별로 정리했어요. 다른 곳에서 먼저 만든 앱을 올리기 전에 확인하세요.

AXHub에 올린 앱은 내 컴퓨터가 아니라 AXHub 서버에서 돌아요. 그래서 내 컴퓨터에서 잘 되던 앱도 아래 조건을 어기면 배포가 실패하거나, 배포는 됐는데 기능이 동작하지 않아요.

규칙마다 무엇을 보면 되는지 · 왜 안 되는지 · 어떻게 고치는지 · 배포가 실패하면 보이는 코드를 적었어요.

AI 에게 맡길 때 — Claude 같은 AI 가 배포 전에 이 문서를 기준으로 앱을 점검해요. 규칙마다 적힌 처리 방식을 따라요.

  • 바로 고침 — 사용자에게 묻지 않고 고치고, 배포 뒤에 "이렇게 맞춰서 올렸어요"로 알려요. 앱이 하는 일은 바뀌지 않아요.
  • 물어봄 — 앱이 하는 일이 바뀌거나, 사용자만 아는 값이 필요하거나, 되돌리기 어려운 선택이에요. 한 번만 묻고, 답을 기억해 다음 배포에는 다시 묻지 않아요.
  • 알림 — 고칠 수 없거나 고칠 필요가 없는 제약이에요. 한 번 알려 주고 진행해요.

이 문서에 없는 문제도 확실하면 보고해요. 다만 테스트·예시 코드·README 에만 있는 흔적은 문제로 보지 않아요.

1. 앱이 뜨는 조건

아래는 Docker 로 실행되는 앱(대부분의 서버 앱) 기준이에요. 정적 사이트는 6절을 보세요.

AXHub 는 배포 뒤 앱 주소로 상태 확인 요청을 보내요. 60초 안에 정상 응답이 오지 않으면 앱을 다시 시작하고, 결국 배포가 실패해요. 이때 실패 코드는 대부분 healthcheck.startup_budget_exceeded("N초 안에 헬스체크에 응답하지 못했어요 … start_period 를 올려 보세요") 이고, 확인이 안 되면 healthcheck.container_crash 예요. 안내 문구는 대기 시간을 늘리라고 하지만, 실제 원인은 1-1 ~ 1-6 중 하나인 경우가 많아요. 대기 시간을 늘리기 전에 먼저 확인해요.

1-1. 포트는 PORT 환경변수로 연다

  • 무엇을 보나 — 서버가 3000, 5000, 8000 같은 숫자를 직접 적어 열고 있는지 (app.listen(3000), uvicorn ... --port 8000).
  • 왜 안 되나 — AXHub 는 앱이 열 포트를 정해 환경변수 PORT 로 넣어 주고, 그 포트로만 상태를 확인해요. 정하는 순서는 axhub.yaml 의 runtime.port → Dockerfile 의 첫 숫자 EXPOSE → 콘솔 앱 설정의 포트 → (자동 Dockerfile 이면) 시작 명령의 --port·${PORT:-N} → 8080 이에요. 앱이 PORT 를 무시하고 다른 포트로 열면 확인이 계속 실패해요.
  • 고치는 법 — process.env.PORT(Node), os.environ["PORT"](Python)로 포트를 읽어요. 포트를 꼭 고정해야 하면 axhub.yaml 에 runtime.port 로 그 숫자를 적어요.
  • 처리 — 바로 고침
  • 실패 코드 — healthcheck.startup_budget_exceeded, healthcheck.container_crash

1-2. 모든 주소(0.0.0.0)에서 연다

  • 무엇을 보나 — 서버가 localhost 나 127.0.0.1 에서만 열려 있는지 (app.run(host="127.0.0.1"), next start -H localhost).
  • 왜 안 되나 — 상태 확인 요청은 서버 밖(파드 주소)에서 와서, 127.0.0.1 에만 열린 서버에 닿지 않아요.
  • 고치는 법 — 0.0.0.0 으로 열어요. Next.js 는 HOSTNAME=0.0.0.0, Flask·FastAPI 는 host="0.0.0.0".
  • 처리 — 바로 고침
  • 실패 코드 — healthcheck.startup_budget_exceeded, healthcheck.container_crash

1-3. 상태 확인 주소는 로그인 없이 빨리 응답한다

  • 무엇을 보나 — 상태 확인 주소(기본 /)가 401·403·404·500 을 주는지, 응답 전에 DB·외부 API 를 부르는지.
  • 왜 안 되나 — 상태 확인은 로그인 정보도 X-AxHub-* 헤더도 없이 HTTP GET 으로 와요. 200~399 응답이 켜지는 동안은 5초, 켜진 뒤에는 3초 안에 와야 해요. 로그인 화면으로 한 번 넘기는 302 는 통과해요 (넘어간 화면이 200 이면). 반복해서 넘기거나, 401·403 을 주면 실패해요. 켜진 뒤 3초를 넘기면 그때마다 앱이 다시 시작될 수 있어요.
  • 고치는 법 — / 가 위 조건을 못 맞추면, 로그인 없이 바로 200 을 주는 가벼운 경로(예: /healthz)를 만들고 axhub.yaml 에 runtime.health_path: /healthz 로 적어요. 이 경로에서는 DB 를 부르지 않아요.
  • 처리 — 바로 고침
  • 실패 코드 — healthcheck.startup_budget_exceeded, healthcheck.container_crash

1-4. 주소 이름 검사가 상태 확인을 막지 않는다

  • 무엇을 보나 — Django 의 ALLOWED_HOSTS 처럼 들어온 요청의 주소 이름(Host)을 검사하는 설정.
  • 왜 안 되나 — 상태 확인 요청의 Host 는 앱 주소가 아니라 파드의 IP 예요. 목록에 없으면 400 을 줘서 확인이 실패해요.
  • 고치는 법 — 상태 확인 경로는 Host 검사에서 빼거나, 허용 목록을 넓혀요 (Django 는 ALLOWED_HOSTS = ["*"] 처럼, 또는 상태 확인용 미들웨어를 앞에 둬요).
  • 처리 — 바로 고침
  • 실패 코드 — healthcheck.startup_budget_exceeded, healthcheck.container_crash

1-5. 60초 안에 켜진다

  • 무엇을 보나 — 시작할 때 오래 걸리는 일을 먼저 하는지 (대량 데이터 적재, 모델 내려받기, 오래 걸리는 마이그레이션). Next.js SSR 처럼 무거운 앱인지.

  • 왜 안 되나 — 배포 뒤 60초 안에 상태 확인에 응답하지 않으면 실패로 봐요. 새 앱 기본 사양은 CPU 0.5코어라 무거운 앱은 켜지는 데 오래 걸릴 수 있어요. Dockerfile 의 HEALTHCHECK 는 읽지 않아요.

  • 고치는 법 — 오래 걸리는 일은 시작 순서에서 빼요. 그래도 오래 걸리면 axhub.yaml 에 대기 시간을 늘리거나(최대 5분, 90s·2m 처럼 단위 필수) 앱 사양을 올려요.

    runtime:
      healthcheck:
        start_period: 120s
  • 처리 — 바로 고침 (매번 데이터를 지우고 다시 넣는 시작 작업처럼 앱 동작이 바뀌면 물어봄)

  • 실패 코드 — healthcheck.startup_budget_exceeded

1-6. 포트는 하나, 평문 HTTP

  • 무엇을 보나 — 앱이 직접 HTTPS 인증서로만 여는지, 관리자 화면을 다른 포트로 따로 여는지.
  • 왜 안 되나 — HTTPS 는 AXHub 가 앞단에서 처리하고 앱은 평문 HTTP 로 받아요. HTTPS 로만 열면 상태 확인이 실패해요. 바깥에는 포트 하나만 열려서, 두 번째 포트의 화면은 배포는 되지만 접속할 수 없어요.
  • 고치는 법 — HTTP 로 열고, 화면은 한 포트로 합쳐요. 서비스가 여럿이면 compose 를 써요.
  • 처리 — 바로 고침
  • 실패 코드 — healthcheck.startup_budget_exceeded (HTTPS 로만 열 때), 없음 (두 번째 포트)

2. 빌드

2-1. 무엇으로 빌드할지 정해져 있어야 한다

  • 무엇을 보나 — 저장소 맨 위에 Dockerfile 이 있는지. 없다면 Node · Python · Go · Ruby · Java 중 하나인지.
  • 왜 안 되나 — AXHub 는 axhub.yaml → 맨 위 Dockerfile → compose 파일 → 자동 감지 순서로 빌드 방법을 정해요. 자동으로 만든 Dockerfile 은 Node 는 node:22-alpine, Python 은 python:3.12-slim 을 써요. 다섯 언어가 아니면(Rust, PHP, .NET 등) 자동 감지가 실패하거나, 감지돼도 도구 없는 이미지로 빌드돼 실패해요. 네이티브 모듈(better-sqlite3 등)은 컴파일 도구(python3·make·g++)가 없어 실패할 수 있어요.
  • 고치는 법 — Dockerfile 을 직접 둬요. 네이티브 모듈이 필요하면 Dockerfile 에 컴파일 도구 설치를 넣거나 순수 JS 대안을 써요.
  • 처리 — 바로 고침
  • 실패 코드 — resolve.spec_invalid (상세: dockerfile not found at repo root: railpack auto-detection failed), build.command_failed, build.native_module_toolchain_missing

2-2. 내 컴퓨터에서 빌드가 끝까지 된다

  • 무엇을 보나 — npm run build 같은 빌드 명령이 오류 없이 끝나는지. 개발 서버(npm run dev)는 타입 오류가 있어도 돌지만 빌드는 멈춰요. 코드가 쓰는 패키지가 package.json·requirements.txt 에 다 있는지.
  • 왜 안 되나 — AXHub 는 빌드 명령을 그대로 실행해요. 타입 오류, 의존성 충돌(ERESOLVE), 없는 명령(exit 127)은 서버에서도 실패해요. 의존성 목록에 없는 패키지를 쓰면 빌드는 되고 시작할 때 죽어요. 빌드는 20분을 넘길 수 없어요.
  • 고치는 법 — 배포 전에 빌드를 한 번 돌려 오류를 고쳐요. 빠진 패키지는 의존성 목록에 더해요. 의존성 충돌은 lockfile 을 커밋하고 같은 패키지 관리자를 써요.
  • 처리 — 바로 고침
  • 실패 코드 — build.command_failed, build.dependency_install_failed, build.timeout, healthcheck.container_crash (빠진 패키지)

2-3. 빌드에는 실행용 환경변수가 없다

  • 무엇을 보나 — 빌드 중에 DATABASE_URL 같은 값을 읽는지 (예: prisma.config.ts 가 빌드 때 env("DATABASE_URL") 를 요구). 직접 쓴 Dockerfile 이 빌드 값을 ARG 없이 쓰는지.
  • 왜 안 되나 — 실행용(runtime) 값은 빌드에 들어가지 않아요. 빌드에 필요한 값은 Stage 를 Build 또는 Both 로 등록해야 하고, 직접 쓴 Dockerfile 은 ARG 이름 을 선언해야 읽혀요.
  • 고치는 법 — 빌드 때 쓰는 값은 Stage 를 Build/Both 로 등록하고 Dockerfile 에 ARG 를 더해요. 빌드에 DB 주소가 꼭 필요하면(Prisma 등) 실제 DB 가 아닌 형식만 맞는 값을 Build 로 등록해요. 실행 중에는 AXHub 가 진짜 값으로 덮어요.
  • 처리 — 바로 고침
  • 실패 코드 — build.env_referenced_at_build_time, build.command_failed

2-4. 앱이 쓰는 파일은 저장소에 올라가 있다

  • 무엇을 보나 — 앱이 읽는 데이터 파일이 .gitignore 에 걸려 빠져 있는지. 마지막 수정이 커밋·push 됐는지. 100MB 넘는 파일, node_modules 처럼 커밋된 대량 파일이 있는지.
  • 왜 안 되나 — 서버는 내 컴퓨터가 아니라 저장소의 코드를 가져가요. 파일 하나 100MB, 전체 200MB, 파일 5만 개를 넘으면 소스를 받지 못해요.
  • 고치는 법 — 필요한 파일을 커밋하고 push 해요. push 직후라 커밋을 못 찾으면 잠시 뒤 다시 배포해요. 큰 데이터는 파일 보관함(storage)이나 DB 에 둬요.
  • 처리 — 커밋·push 는 바로 고침, 큰 데이터 옮기기는 물어봄
  • 실패 코드 — resolve.commit_not_found, build.context_file_missing, prepare.source_package_failed, healthcheck.container_crash (시작할 때 파일을 읽다 죽음)

2-5. 배포 방식은 앱을 만들 때 정해지고 바꿀 수 없다

배포 방식은 셋이에요: docker(컨테이너 하나), compose(서비스 여러 개), static(정적 사이트). 앱을 만들 때 정해지고, 나중에 바꿀 수 없어요. 저장소 내용으로 판정한 방식이 앱의 방식과 다르면 배포가 실패해요.

  • 무엇을 보나 — 로컬 DB 를 띄우려고 만든 docker-compose.yml 이 맨 위에 있고 Dockerfile 이 없는지. 이미 만든 앱의 방식과 저장소 모양이 맞는지.
  • 왜 안 되나 — Dockerfile 이 없으면 compose 파일을 앱 전체로 보고 compose 로 판정해요. docker 로 만든 앱이면 방식이 어긋나 실패하고, 로컬 전용 DB 만 올라가요.
  • 고치는 법 — 로컬 전용 compose 파일이면 docker-compose.dev.yml 처럼 이름을 바꾸거나, 맨 위에 Dockerfile 을 둬요 (Dockerfile 이 있으면 compose 파일은 보지 않아요). 정말로 다른 방식이 필요하면 새 앱으로 다시 만들어야 해요.
  • 처리 — 파일 이름 바꾸기·Dockerfile 추가는 바로 고침. 새 앱으로 다시 만들기는 물어봄.
  • 실패 코드 — resolve.spec_invalid (상세: deploy method mismatch)

3. 설정

3-1. axhub.yaml 은 맨 위에, 아는 필드만

  • 무엇을 보나 — axhub.yaml 이 하위 폴더에 있는지, 모르는 필드(오타 포함)가 있는지.
  • 왜 안 되나 — 저장소 맨 위 파일만 읽고, 모르는 필드가 하나라도 있으면 파일 전체를 거부해요. 크기는 8KB 까지예요.
  • 고치는 법 — 앱이 저장소의 하위 폴더에 있으면 파일을 옮기지 말고, 맨 위 axhub.yaml 에 root: 하위폴더 를 적어요. 필드는 axhub.yaml 레퍼런스에 있는 것만 써요. runtime.healthcheck 의 시간은 90s 처럼 단위를 붙이고, ci.timeout 은 숫자(초)로 써요.
  • 처리 — 바로 고침
  • 실패 코드 — resolve.spec_invalid (상세: axhub.yaml 파싱 실패 또는 axhub.yaml의 …)

3-2. 필요한 환경변수는 AXHub 에 등록돼 있다

  • 무엇을 보나 — 코드가 읽는 환경변수(process.env.X, os.environ["X"])와 axhub.yaml 의 env.required 가 AXHub 에 등록돼 있는지. .env 파일에만 있는지.
  • 왜 안 되나 — .env 파일은 서버로 올라가지 않아요. env.required 에 적은 값이 없으면 배포가 시작 전에 멈추고, 적지 않은 값이 없으면 읽는 방식에 따라 켜지다 죽거나 그 기능만 실패해요.
  • 고치는 법 — 값을 AXHub 에 등록하고 다시 배포해요. 이름은 대문자·숫자·_ 만, AXHUB_ 로 시작할 수 없어요.
  • 처리 — 내 컴퓨터의 .env 에 값이 있으면 바로 등록해요. 단 내 컴퓨터를 가리키는 값(localhost, 127.0.0.1, 사설 IP)과 3-3의 이름은 등록하지 않아요. 값을 어디서도 찾을 수 없으면 물어봄.
  • 실패 코드 — prepare.required_env_missing, healthcheck.container_crash

3-3. AXHub 가 넣어 주는 값은 실행용으로 넣지 않는다

아래 이름은 조건이 맞으면 AXHub 가 실행 중인 앱에 자동으로 넣고, 같은 이름으로 등록한 값보다 우선해요. 빌드에는 들어가지 않아요.

이름언제 들어오나
PORT항상 (Docker 앱)
DATABASE_URL, DIRECT_DATABASE_URL그 환경에 AXHub DB 가 있을 때 (database.engine: postgres 로 만들어져요)
STORAGE_ENDPOINT, STORAGE_BUCKET, STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY파일 보관함이 있을 때 (storage.enabled: true 로 만들어져요)
STORAGE_CDN_BASE_URL, STORAGE_CDN_SIGN_KEYstorage.cdn: true 일 때
AXHUB_RELAY_SIGNING_SECRET항상
AXHUB_APP_TOKEN, AXHUB_API_URL알림·메일 기능을 쓸 수 있을 때
  • 처리 — 이 이름을 .env 에서 옮겨 실행용으로 등록하지 않아요. env.required 에도 적지 않아요. 빌드에 필요하면 2-3대로 Build 로 등록해요. 바로 고침.

4. 데이터 저장

4-1. AXHub DB 는 빈 상태로 시작한다

  • 무엇을 보나 — 앱이 테이블을 만드는 코드(CREATE TABLE IF NOT EXISTS, ORM 마이그레이션)가 있는지. 내 컴퓨터 DB 에서 손으로 만든 테이블에 기대고 있는지.
  • 왜 안 되나 — AXHub DB 는 운영·스테이징마다 따로, 빈 상태로 만들어져요. 테이블을 만드는 코드가 없으면 배포는 성공하고 첫 요청부터 500 이 나요.
  • 고치는 법 — 앱이 켜질 때 테이블을 만들게 하거나(CREATE TABLE IF NOT EXISTS), 마이그레이션을 시작 순서에 넣어요 (60초 안에 끝나야 해요). Prisma 같은 마이그레이션 도구는 DIRECT_DATABASE_URL 로 연결해요 — DATABASE_URL 은 트랜잭션 단위 중계기라 마이그레이션 잠금이 동작하지 않아요. 두 주소 모두 SSL 을 쓰지 않으므로(sslmode=disable) SSL 을 강제하는 연결 설정은 빼요.
  • 처리 — 바로 고침
  • 실패 코드 — 없음 (배포는 성공하고 요청이 500)

4-2. 서버 폴더에 저장한 것은 사라진다

  • 무엇을 보나 — SQLite(*.db, better-sqlite3, sqlite3), JSON 파일에 데이터를 쌓는 코드(lowdb 등), 업로드를 앱 폴더(./uploads)에 저장하는 코드.

  • 왜 안 되나 — Docker 앱의 일반 폴더는 다시 배포하거나, 앱이 재시작되거나, 절전에서 깨어나면 처음 상태로 돌아가요.

  • 고치는 법 — 데이터는 AXHub DB(PostgreSQL)로, 파일은 파일 보관함으로 옮겨요.

    database:
      engine: postgres   # DATABASE_URL 이 자동으로 들어와요
    storage:
      enabled: true      # STORAGE_* 가 자동으로 들어와요
  • 처리 — 물어봄 ("AXHub DB 로 바꿀까요, 그대로 올릴까요?"). 보관이 필요 없는 캐시, 테스트 코드에서만 쓰는 SQLite 는 그대로 둬요.

  • 실패 코드 — 없음 (배포는 성공하고 데이터만 사라져요)

4-3. DB 는 PostgreSQL 하나

  • 무엇을 보나 — MySQL·MongoDB 를 쓰는지, 지원하지 않는 확장을 쓰는지.
  • 왜 안 되나 — AXHub DB 는 PostgreSQL 만 돼요. 확장은 pgvector, pg_trgm, citext, unaccent, fuzzystrmatch, uuid-ossp, pgcrypto, hstore, ltree, intarray, tablefunc, btree_gin, btree_gist 만 쓸 수 있어요.
  • 고치는 법 — PostgreSQL 로 옮기고, 확장은 database.extensions 에 적어요.
  • 처리 — 물어봄
  • 실패 코드 — resolve.spec_invalid (목록에 없는 확장), prepare.raw_db_extension_failed (설치 실패)

5. 네트워크

5-1. 밖으로는 널리 쓰는 포트만 나간다

  • 무엇을 보나 — 메일 서버 25번 포트로 직접 발송, 아래 목록에 없는 포트(예: 8080, 9200) 로 외부 연결, 사설 IP(10.x, 172.16~31.x, 192.168.x)·localhost 의 다른 프로그램 호출.

  • 왜 안 되나 — 보안을 위해 앱에서 밖으로 나가는 연결은 공인 인터넷의 아래 TCP 포트만 열려 있어요. 막힌 연결은 오류 없이 시간 초과로 끝나요. 시작할 때 연결하면 앱이 켜지지 못하고, 요청 중에 연결하면 그 기능만 실패해요.

    용도포트
    웹443, 80
    메일 보내기465, 587 (25 는 막혀 있어요)
    메일 받기993, 995
    SSH·SFTP22
    외부 DB3306 (MySQL), 5432 (PostgreSQL), 6379 (Redis), 27017 (MongoDB)

    이 포트는 앱을 다시 배포할 때 적용돼요. 2026-10-16 전에 마지막으로 배포한 앱은 443 만 열려 있으니 한 번 다시 배포해요.

  • 고치는 법 — 메일은 465·587 로 보내요(nodemailer 의 port: 587 등). 데이터는 가능하면 AXHub DB 를 쓰고, 외부 DB 를 꼭 써야 하면 그 서비스가 공인 주소로 접속을 허용하는지 확인해요. 회사 내부망·내 컴퓨터는 연결할 수 없어요 (8절).

  • 처리 — 물어봄 (연결 대상을 바꾸는 일이라). 바꿀 수 없으면 알림.

  • 실패 코드 — healthcheck.startup_budget_exceeded, healthcheck.container_crash (시작할 때 연결하면), 없음 (요청 중에 연결하면)

5-2. 요청 크기와 응답 시간

  • 무엇을 보나 — 요청 하나로 50MB 넘는 파일을 받는지, 요청 하나 안에서 2분 넘게 걸리는 일을 하는지(긴 AI 일괄 처리, 큰 리포트 생성).
  • 왜 안 되나 — 요청 본문은 50MB 까지, 첫 응답은 120초 안에 와야 해요. 넘으면 413·504 로 끊겨요.
  • 고치는 법 — 큰 파일은 파일 보관함에 바로 올리게 하고(미리 서명된 주소), 오래 걸리는 일은 "작업 시작 → 진행 상황 조회"로 나누거나 응답을 조금씩 흘려보내요.
  • 처리 — 물어봄
  • 실패 코드 — 없음 (런타임 413·504)

5-3. 비공개 앱은 외부 웹훅을 직접 받지 못한다

  • 무엇을 보나 — 결제·GitHub·메신저 같은 외부 서비스가 앱으로 요청을 보내는 경로(/api/webhook, /hooks/…, Stripe·Slack 서명 검증 코드).

  • 왜 안 되나 — 비공개 앱 주소로 오는 요청은 모두 AXHub 로그인을 거쳐요. 로그인하지 않은 외부 서버의 요청은 로그인 화면으로 넘겨져 앱까지 오지 않아요. 배포는 성공하고 웹훅만 도착하지 않아요.

  • 고치는 법 — 접수창구를 만들어 그 주소를 외부 서비스에 알려 줘요. 앱은 비공개 그대로 두고, 검증을 통과한 요청만 앱의 수신 경로로 전달돼요. 전달된 요청은 AXHUB_RELAY_SIGNING_SECRET 으로 서명을 확인해요 (웹훅 받기).

    axhub relay create --app <앱> --name <창구-이름> --verify-mode key --target-path /api/webhook
  • 처리 — 물어봄 (외부 서비스에 새 주소를 등록해야 해서)

  • 실패 코드 — 없음 (배포는 성공하고 웹훅만 안 와요)

5-4. AI 는 AXRouter 로 부른다

  • 무엇을 보나 — Ollama·LM Studio 같은 로컬 AI 서버 주소(localhost:11434), 로컬 임베딩·음성 인식(whisper.cpp) 호출, 회사에서 쓰는 AI 키가 따로 없는지.
  • 왜 안 되나 — 서버에는 로컬 AI 가 없어요 (8절). 회사가 AXRouter 연동을 켜 두었으면, 개인 AI 키 없이 회사 AI 게이트웨이(OpenAI 호환)로 부를 수 있어요.
  • 고치는 법 — 먼저 그 회사가 AXRouter 를 쓰는지 확인해요. axhub axrouter status --tenant <회사> --json 의 data.available 이에요.
    • true — 키를 발급해 비밀 환경변수로 등록하고, 앱은 OpenAI 호환 방식으로 불러요. 대화(/chat/completions)·임베딩(/embeddings)·받아쓰기(/audio/transcriptions) 모두 같은 주소예요.

      axhub axrouter keys issue --name "<앱> 서버용"     # 키는 이때 한 번만 보여요
      axhub env set LLM_API_KEY --app <앱> --secret --stage runtime --from-stdin
      axhub env set LLM_BASE_URL https://axrouter.ai/v1 --app <앱> --plain --stage runtime

      쓸 수 있는 모델 이름은 GET $LLM_BASE_URL/models 로 봐요. 사용량은 키를 발급한 사람의 AI 예산으로 잡혀요.

    • false 또는 확인 실패 — AXRouter 를 권하지 않아요(회사가 연동을 꺼 둔 경우예요). 사용자가 쓰는 AI 서비스의 본인 키를 비밀 환경변수로 넣어요.

      axhub env set OPENAI_API_KEY --app <앱> --secret --stage runtime --from-stdin   # 또는 ANTHROPIC_API_KEY 등
  • 처리 — 물어봄 (어느 AI 로 바꿀지, 누구 키로 쓸지)
  • 실패 코드 — 없음 (그 기능만 실패해요)

6. 정적 사이트

  • 무엇을 보나 — 서버 없이 HTML·JS 만 올리는 사이트인지, 그리고 앱을 정적 사이트로 만들었는지.

  • 왜 안 되나 — 정적 호스팅은 앱을 정적 사이트(static)로 만들었을 때만 써요. 빌드 결과 폴더(기본 dist)에 index.html 이 있어야 하고, 파일 2,000개·파일당 50MB·합계 500MB 까지예요. DB·파일 보관함은 쓸 수 없어요. docker 로 만든 앱에 deploy_method: static 을 적으면 방식이 어긋나 실패해요 (2-5).

  • 고치는 법 — 정적 앱이면 아래처럼 적어요. docker 로 만든 앱이라면 그대로 docker 로 둬도 Vite 같은 사이트는 자동 감지로 떠요.

    build:
      deploy_method: static
      static_output_dir: dist

    빌드 없이 루트에 index.html이 있는 사이트는 static_output_dir: .이에요. 작업공간이 Axhub 저장소를 쓰면 이 설정을 담은 소스를 앱 저장소에 push해서 배포해요 — 저장소 화면에서 코드와 커밋별 배포 결과를 볼 수 있어요.

  • 처리 — 정적 앱의 설정 맞추기는 바로 고침. 방식을 바꾸는 일은 물어봄.

  • 실패 코드 — resolve.spec_invalid (상세: deploy method mismatch), apply.static_release_failed, prepare.database_unsupported_for_static, prepare.storage_unsupported_for_static

7. 리소스와 절전

  • 사양 — 새 앱 기본 사양은 CPU 0.5코어·메모리 1GB 예요. 메모리를 넘으면 앱이 강제로 재시작돼요(healthcheck.container_crash). 브라우저(PDF 만들기)·OCR·큰 파일 처리처럼 무거운 일을 하면 사양을 올려요. 실행 중인 앱에 바로 반영돼요 (최대 16GB, 회사가 정한 상한이 있으면 그 아래까지).

    axhub apps update <앱> --resource-tier M   # XS 0.5코어·1GB · S 1코어·2GB · M 2코어·4GB · L 3코어·8GB · XL 4코어·16GB
  • 임시 저장공간 — 기본 1GB 예요. 시작할 때 큰 파일을 내려받으면 넘칠 수 있어요. runtime.ephemeral_storage: 4Gi 처럼 최대 10GB 까지 늘릴 수 있어요.

  • 예약 작업 — AXHub 에는 따로 예약 실행(cron) 기능이 없어요. 앱 안에서 돌려요 — Node 는 node-cron, Python 은 APScheduler 처럼 앱이 켜질 때 스케줄러를 같이 띄워요. 레플리카가 2 이상이면 작업이 두 번 돌아요. compose 앱이면 스케줄러 서비스를 하나 두고 그 서비스만 레플리카 1로 둬요 (레플리카). 재시작 중에 놓친 실행을 앱이 켜질 때 따라잡게 해 두면 안전해요.

  • 절전 — 운영 앱은 72시간 동안 아무도 접속하지 않으면 절전될 수 있어요. 절전 중에는 앱 안의 예약 작업(cron)·백그라운드 작업도 멈춰요. 다음 접속 때 깨어나요.

  • 처리 — 알림

8. AXHub 에서 안 되는 6가지

아래는 구조상 AXHub 서버에서 할 수 없는 일이에요. 코드에서 흔적을 찾으면 사용자에게 알리고, 대신 쓸 방법을 물어요.

앱에 이런 기능이 있으면코드에서 보이는 흔적대신 이렇게
내 컴퓨터의 파일·프로그램을 사용C:\, /Users/, 바탕화면 경로, 앱 자신이 아닌 localhost:포트 호출파일을 저장소에 넣거나 화면에서 업로드받기
회사 내부 시스템에 연결사설 IP, .local·.internal·.corp 주소, 인트라넷 주소예시 데이터를 앱에 넣어 사용
앱 안에서 AI 모델을 직접 실행Ollama, llama.cpp, vLLM, transformers 모델 내려받기, diffusers, GPU 코드AXRouter 같은 AI API 로 호출 (5-4)
웹사이트가 아닌 형태의 앱Electron·Tkinter·PyQt, React Native·Expo·Flutter, 크롬 확장 manifest.json, 엑셀 매크로같은 기능을 웹 화면으로
엑셀·한글 프로그램을 열어 작업win32com, pywin32, xlwings, comtypes, 한글 자동화, AppleScript파일만 읽고 쓰는 라이브러리(openpyxl 등)
입력한 내용을 서버 파일에 저장4-2 참고AXHub DB · 파일 보관함
  • 처리 — 처음 발견하면 물어봄. 사용자가 "그대로 올려"라고 하면 기억해서 다시 묻지 않아요. 테스트·예시 코드·README 에만 있는 흔적은 무시해요.

9. 실패 코드로 규칙 찾기

배포가 실패하면 실패 코드로 위 규칙을 찾아요.

실패 코드먼저 볼 규칙
resolve.commit_not_found2-4 (커밋을 push 했는지, push 직후라면 잠시 뒤 다시)
resolve.spec_invalid2-1, 2-5, 3-1, 4-3, 6 (상세 문구를 같이 봐요)
prepare.required_env_missing3-2, 3-3
prepare.source_package_failed2-4
prepare.database_unsupported_for_static, prepare.storage_unsupported_for_static6
prepare.raw_db_extension_failed4-3
build.command_failed, build.dependency_install_failed, build.timeout2-2, 2-1
build.native_module_toolchain_missing2-1
build.env_referenced_at_build_time2-3
build.context_file_missing, build.dockerfile_parse_error2-4 (Dockerfile 의 COPY 경로·.dockerignore), Dockerfile 문법
ci.tests_failedaxhub.yaml 의 ci.commands 가 실패했어요 — 설치 명령을 앞에 뒀는지
healthcheck.startup_budget_exceeded1-1, 1-2, 1-3, 1-4, 1-6, 5-1 을 먼저, 그다음 1-5
healthcheck.container_crash2-2(빠진 패키지), 2-4(빠진 파일), 3-2, 5-1, 7(메모리), 1-3
healthcheck.rollout_timeout앱이 켜졌지만 5분 동안 준비 상태가 안 됐어요 — 1-3 (켜진 뒤 3초)
healthcheck.capacity_wait_timeout앱 문제가 아니에요. 잠시 뒤 다시 배포해요
healthcheck.image_pull_failedcompose 의 image: 이름·접근 권한
apply.static_release_failed6