K

axhub.yaml

Canonical deploy manifest schema from the current ax-hub-backend manifest domain

axhub.yaml is the deploy contract file you commit to the repository. Apps created from a template already ship with this file set up, so you rarely need to touch it. Look up the exact fields on this page when you want to pin the build commands yourself, declare an app-owned database or storage, or pick the compose entry service. Deploys work without the file too — AxHub inspects the repository and decides the build method on its own, and that order is on this page as well. For the step-by-step of wiring each feature into your app, see the Install the SDK.

Minimal example

axhub.yaml
version: axhub/v1
name: hello-axhub
root: web   # only when the app lives in a subfolder (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: DATABASE_URL
      scope: runtime
  optional:
    - name: NEXT_PUBLIC_API_URL
      scope: build
ci:
  commands:
    - npm test
  timeout: 300

All fields are optional. Unset values fall back to DB settings or resolver defaults.

root — when the app lives in a subfolder

If one repository holds several projects and the thing you deploy is in a subfolder (say web/), add one line to the axhub.yaml at the repository root.

axhub.yaml (repository root)
root: web

That one line makes the folder behave as the app's top level. You can drop the workaround Dockerfile you kept at the root and get automatic detection and Dockerfile generation instead.

What becomes relative to root

  • Detection of Dockerfiles, compose files, and framework markers (package.json and friends)
  • Generated Dockerfiles and the build context (what gets copied, which .dockerignore applies)
  • The other path fields in this file — build.dockerfile, build.compose_file, build.static_output_dir, metadata.icon
  • Where CI commands and static site builds run

So root: web plus dockerfile: docker/prod.Dockerfile actually reads web/docker/prod.Dockerfile. You never have to remember which fields are repo-relative and which are app-relative.

Rules

What you writeResult
web, ./web, web/All treated as web
web/appMore than one level deep is fine
., ./Same as not declaring it (repository root)
/web, ../otherFails before the deploy — only relative paths inside the repo
A folder with no files in itFails before the deploy

Config is read only from the axhub.yaml at the repository root. A web/axhub.yaml sitting alongside is ignored, with one warning line on the deploy. Conversely, if there's no file at the root and only web/axhub.yaml exists, that counts as no root declaration at all.

  • With root declared, a Dockerfile at the repository root is ignored — root is the app's boundary
  • It's a repository setting rather than a console one, so a single push applies it (no reconnecting, no console steps)
  • Security scanning still covers the whole repository — the scope isn't narrowed
  • Apps without a root declaration behave exactly as they did before this existed

Files and precedence

ItemCurrent contract
canonical filenameaxhub.yaml
max size8 KiB
prioritymanifest > DB AppSpecData > resolver default
deploy resolutionmanifest → Dockerfile → Compose → Railpack auto-detection

How the build method is decided

AxHub decides how to build by looking at which files exist at the repository root. It checks in the order below, top to bottom, and stops at the first match.

axhub.yaml있으면 이 설정대로Dockerfile단일 이미지 빌드Compose 파일docker-compose.yml 등자동 감지package.json 등으로 추론위에서부터 검사해서 처음 맞는 방법 하나로 빌드해요
빌드 방법이 정해지는 순서

Here is exactly what each step looks at.

  1. axhub.yaml — if present, this manifest wins. A build.dockerfile override and strategy: pinned + start pinned execution take effect at this step.
  2. Dockerfile — if present at the root, the app is built as a single image.
  3. Compose file — searched in the order docker-compose.yml · docker-compose.yaml · compose.yaml · compose.yml.
  4. Railpack auto-detection — if none of the above exist, the framework is inferred from marker files: package.json→node · go.mod→go · requirements.txt/pyproject.toml→python · Gemfile→ruby · pom.xml/Gradle files→java·kotlin · Cargo.toml→rust.

If none of the four match, no build method can be found and the deploy fails. To pin the build/run commands yourself instead of leaving them to guesswork, put an axhub.yaml in the repository — start from the minimal example.

Top-level fields

FieldTypeDescription
versionstringDocumented value: axhub/v1. Unknown versions may be accepted for forward compatibility, but known fields are still checked.
namestringDisplay-only label. Not enforced against stored app name.
rootstringThe folder to treat as the app's top level when the app lives in a subfolder (monorepo).
metadataobjectApp name, description, and icon kept in the repo so they travel with the code.
runtimeobjectPort, health probe path, and the compose entry service (entry_service).
buildobjectBuild/deploy method and command hints.
databaseobjectApp-owned DB request — engine: postgres.
storageobjectApp-owned file storage request — enabled: true.
envobjectRequired/optional env names and scopes. Values never live here.
ciobjectPre-build CI commands and timeout.

runtime

FieldTypeLimit
portinteger1–65535. Falls back to Dockerfile EXPOSE or adapter default. Ignored in compose mode — the compose file decides the ports.
entry_servicestringCompose only. The service to connect to the outside address. Ignored in docker/static mode. See Compose entry service below for the rules.
health_pathstringLiveness/readiness probe path. Empty falls back to /.
replicasmapcompose only. Service name → pod count. Values are 1 or greater; services you omit follow the platform default.

Replicas live here rather than in the compose file's deploy.replicas on purpose. The compose file stays pure so docker compose up works locally, and "how to deploy" belongs to the manifest.

runtime:
  replicas:
    web: 2
    beat: 1      # one scheduler — two would fire every job twice
    redis: 1

Pin schedulers and shared stores to 1. Without per-service values they all scale together, and a cron service at 2 replicas runs every job twice.

build

FieldValuesDescription
strategy`autopinned`
framework`nodepython
installstringInstall command for framework presets.
buildstringBuild command.
startstringRuntime start command.
dockerfilepathDockerfile path override.
deploy_method`dockercompose
compose_filepathCompose file for compose mode. Default docker-compose.yml.
static_output_dirpathBuild output directory uploaded in static mode. Default dist.

deploy_method is immutable on the app DB row, and a manifest value that disagrees with it fails the deploy — changing it usually means recreating the app.

Compose entry service

In a multi-service compose app, only one service connects to the outside address ({app}.{tenant}.…). If you don't declare one, the first service that publishes a host port in the compose file becomes the entry point.

To stop depending on service order, declare it in axhub.yaml (recommended):

runtime:
  entry_service: backend   # compose only — this service gets the outside address
  • With a declaration, a service without a published host port can be the entry point too — whether it uses ports: [{target: 8080}] or expose: ["8080"], that service is opened, using the first declared port.
  • If the declared name is not in the compose file, the deploy fails (build.entry_service_not_found — the failure message includes the actual service list).
  • If the declared service has no ports at all, it also fails (build.entry_service_has_no_port).
  • With no declaration, the existing order-based rule applies unchanged.

What each port style actually does

A service that isn't an "entry candidate" still keeps its internal ports — the two are separate axes. Services reach each other by service name (http://backend:8080).

StyleEntry candidateInternal port
ports: ["3000:3000"]✅ 30003000
ports: [{target: 7000, published: "7000"}]✅ 70007000
ports: ["4000"] (short syntax, no host port)4000
ports: [{target: 6000}] (no published)6000
expose: ["5000"]5000
No ports declared❌ none

A service with no ports declared cannot be reached by name from its siblings. Local docker compose connects services without declarations, so this tends to be a works-only-locally trap — the deploy result carries a warning when it happens. If the service isn't a server (e.g. a queue worker), ignore it.

expose: is absorbed as internal-only ports — internal traffic works, but it never becomes the entry point. Ranges (3000-3005) and udp are unsupported and ignored with a warning.

To move the entry point, move the published host port (or change the entry_service declaration) and redeploy — it switches in one deploy. Address, certificate, and domain settings are per-app and stay put.

Warnings on the deploy result

Warnings never block a deploy — if the setup is intentional, ignore them.

WarningMeaningIf intentional
Multiple services publish portsOnly one connects to the outside addressSwitch the others to target: / expose: to make intent explicit
Service can't be reached by nameIt declares no portsFine for workers/batch jobs
This app won't open at an addressNo service publishes a host portNormal for batch/worker-only apps
Can't read an expose entryRanges/udp are unsupportedSplit into single port numbers
Declared service is connectedentry_service won, and another service publishes a portIgnore if intended

metadata

Keep the app's name, description, and icon in the repository and they travel with the code — no filling them in from the console.

metadata:
  name: Company Wiki
  description: One place to find the team's documents
  icon: assets/icon.png
  icon_dark: assets/icon-dark.png
FieldWhat it is
nameThe app's real name. A different field from the top-level name, which is a display-only label
descriptionThe description shown in the store and lists
icon, icon_darkImage paths inside the repo. Relative to root when you've declared one
  • All optional, and applied when the build actually reaches production — either a direct production deploy or the promote that lands a staging build
  • Leaving a field blank means "don't touch it." You can't clear an existing value by setting it empty

database

Request an app-owned Postgres database from the manifest. Deploying with the declaration creates the database and injects DATABASE_URL (queries) and DIRECT_DATABASE_URL (migrations) into the runtime.

database:
  engine: postgres
FieldValuesDescription
enginepostgresThe only supported engine today. Equivalent to CLI apps raw-db enable.
  • Production and staging get separate databases — each is created when you deploy to that environment.
  • Removing the declaration keeps the database and injection. Turning it off is a console/CLI action (apps raw-db disable).
  • A static-hosting app (deploy_method: static) declaring this fails the deploy with prepare.database_unsupported_for_static.
  • Usage walkthrough: the Connect a database.

storage

Request app-owned S3-compatible file storage from the manifest. Deploying with the declaration creates the app's bucket and injects STORAGE_ENDPOINT, STORAGE_BUCKET, STORAGE_ACCESS_KEY, and STORAGE_SECRET_KEY into the runtime.

storage:
  enabled: true
FieldValuesDescription
enabledbooleantrue provisions on the next deploy. The only field — unknown keys are rejected before the deploy runs.
  • There is one bucket per app, shared by production and staging.
  • Removing the declaration keeps the bucket, files, and injection. Everything is removed only on permanent app deletion.
  • A static-hosting app declaring this fails the deploy with prepare.storage_unsupported_for_static.
  • Usage, console API, and limits: the Connect storage.

env

The manifest declares env names and scopes only. Store values via axhub env set/update.

env:
  required:
    - name: DATABASE_URL
      scope: runtime
    - name: NEXT_PUBLIC_API_URL
      scope: build
  optional:
    - name: SENTRY_DSN
      scope: both
ScopeInjection
buildbuild args only
runtimepod env only
bothrequired in both channels

Omitting scope defaults it to runtime. Names cannot be empty or contain newline/= characters.

ci

FieldLimit
commandsmax 10
timeout1–600 seconds (validation range)

CI commands run before the image build. Failure stops the deploy.

The timeout value is currently validated but not consumed — the pipeline's CI stage budget is fixed at 10 minutes.