Ajo Server

Deploy an App

Prepare an image, create a token and deploy with kit deploy.

ajo-kit-server adds kit deploy to a Kit application. The command builds the App’s Containerfile with Podman, saves an OCI archive and hands it to an Ajo Server host, either through the administration App’s HTTPS API with an App-scoped token or over SSH. Both paths end in the same host deployment supervisor. This page follows a notes App from installation to a confirmed deployment.

Install the plugin

Use ajo-kit 0.3.2 or newer and install the plugin as a development dependency of the App that will run the command:

Terminal
pnpm add ajo-kit@0.3.2
pnpm add -D ajo-kit-server@0.1.1
pnpm exec kit deploy --help

Run these from the App root, where package.json, the Containerfile and the installed plugin live. The App must already use ajo and ajo-kit, with Vite available for Kit’s command line, and its build environment must install development dependencies. No configuration is needed: Kit discovers the plugin among the installed packages.

What the plugin adds to the build

The plugin carries its own engine metadata, which Kit merges into the App’s compiler descriptor:

node_modules/ajo-kit-server/package.json (excerpt)
{
  "kit": {
    "engine": {
      "env": { "optional": ["AJO_ORIGINS_FILE"] },
      "fs": { "roots": ["/ajo/origin"] }
    }
  }
}

The host mounts a read-only directory at /ajo/origin for every App, including Apps without custom domains, and points AJO_ORIGINS_FILE at the origins manifest inside it. This tells the App which request origins are current without changing its canonical APP_URL. Rebuild the App after updating the plugin, and use a host release that provides this mount. To run such an artifact directly on the engine, provide the same directory; kit dev does not need it.

Prerequisites

RequirementToken modeSSH mode
Node 22.18 or newerYesYes
Git, with a named branch checked outYesYes
A Podman installation that can build and save imagesYesYes
An SSH client and a POSIX shellNoYes

On Windows, run SSH deployments from a Linux environment; the command refuses SSH mode there. Token mode needs only Node and Podman.

Prepare the image

kit deploy runs Podman build and save. It does not run kit build or compile the App, so seal the production artifact first, with an engine and compiler from the same verified release:

Terminal
pnpm exec kit build --compiler /absolute/path/to/ajo-engine-compiler

The compiler writes dist/ajo, and the Containerfile must include that artifact and the matching engine. A standalone App builds from its own directory. An App inside a workspace builds from the root that holds pnpm-workspace.yaml, so its COPY paths are relative to that root.

For a standalone App, place the engine next to the artifact and make both readable by the container user:

Terminal
mkdir -p runtime
cp /absolute/path/to/ajo-engine runtime/ajo-engine
chmod 755 runtime/ajo-engine
chmod -R a+rX dist/ajo

A minimal Containerfile is:

Containerfile
FROM scratch
COPY runtime/ajo-engine /bin/ajo-engine
COPY dist/ajo /app
USER 1000:1000
ENV NODE_ENV=production
EXPOSE 8080
ENTRYPOINT ["/bin/ajo-engine"]
CMD ["/app"]

This website uses a fuller version. It gives the artifact to the container user, sets the listen address explicitly and keeps the engine’s ISC and third-party notices, which must accompany a redistributed image:

Terminal
mkdir -p runtime/notices/licenses
cp /path/to/ajo-toolchain/node_modules/ajo-engine/bin/ajo-engine runtime/ajo-engine
cp /path/to/ajo-toolchain/node_modules/ajo-engine/LICENSE /path/to/ajo-toolchain/node_modules/ajo-engine/THIRD_PARTY.md runtime/notices/
cp -R /path/to/ajo-toolchain/node_modules/ajo-engine/licenses/. runtime/notices/licenses/
chmod 755 runtime/ajo-engine
Containerfile
FROM scratch
COPY runtime/ajo-engine /bin/ajo-engine
COPY runtime/notices /usr/share/licenses/ajo-engine
COPY --chown=1000:1000 dist/ajo /app
USER 1000:1000
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=8080
EXPOSE 8080
ENTRYPOINT ["/bin/ajo-engine"]
CMD ["/app"]

An allowlist keeps everything else, credentials included, out of the build context:

.containerignore
*
!Containerfile
!runtime/
runtime/*
!runtime/ajo-engine
!runtime/notices/
!runtime/notices/**
!dist/
dist/*
!dist/ajo/
!dist/ajo/**

Two additions depend on what the App does:

  • SQLite on disk. Declare /ajo/data as a filesystem root and add ENV AJO_DATA=/ajo/data DATABASE_PATH=notes.sqlite. The host mounts each App’s own persistent data directory at /ajo/data. See the data root.
  • Outbound HTTPS, for example mail delivery. The engine verifies TLS peers, so copy a verified CA bundle to /etc/ssl/certs/ca-certificates.crt and add it to the allowlist.

Configure the host first

Token deployment delivers an image. It does not configure the App’s environment, and the token grants no host administration.

  • The App needs NODE_ENV=production and an APP_URL. The operator prepares the App URL and any secrets in the host’s environment file for the App before its first deployment. Without a configured APP_URL, the host uses the App’s generated address. Afterwards, an Owner can replace secrets from the administration App; see App secrets.
  • The App must listen on 0.0.0.0:8080. Kit’s defaults for HOST and PORT already match.
  • Readiness is a successful response to GET /. When a new version does not answer, the host attempts to restore the previous one.
  • The container’s root filesystem is read-only; persistent writes belong under the data root.

Deploy with a token

  1. In the administration App, open Profile, then Tokens, and create a credential for the App. It names one App base name, which covers its production, staging and preview targets. Its permission is fixed to apps:deploy and it expires after 90 days. The value appears only once.

  2. Store it in a private file outside the build context, readable only by you:

    Terminal
    chmod 600 /private/notes-deploy-token
  3. From a named Git branch, pass the file’s path, not the credential itself:

    Terminal
    pnpm exec kit deploy https://panel.example.com --token /private/notes-deploy-token --name notes

The command and the host enforce a few rules around the credential:

  • Before it invokes Podman, the command refuses a token file with group or other access on POSIX systems, and a token file inside the build context, including a parent workspace.
  • The destination is the admin’s HTTPS origin, optionally with a port, and nothing else: no path, query, fragment or embedded credentials. Certificates are verified and redirects are refused.
  • The token must belong to the selected App and still hold apps:deploy. Token deployment cannot replace the administration App, the App serving the host’s own name or an App with privileged host mounts.

The client then hashes the archive, reserves an upload, sends ordered 512 KiB chunks and requires the host to verify the full digest before it submits the deployment. An archive can be at most 128 MiB. The admin keeps one retained upload per account, and an upload expires 15 minutes after it is created.

The deployment ID is printed before submission. Success is reported only after the host’s terminal receipt confirms it. A version that fails readiness is a failed deployment, even when the rollback that follows succeeds.

Resume observation

If observation is interrupted, resume it with the same credential, App name and Git branch:

Terminal
pnpm exec kit deploy https://panel.example.com --token /private/notes-deploy-token --name notes --resume <id>

--resume observes the existing receipt and never builds, uploads or submits again. After a verified terminal result, it can clean up the upload. Observation is bounded to 15 minutes; a timeout means the result is unconfirmed, so keep the ID and ask again later.

Target selection

The Git branch chooses the target. The derivation is printed before anything is built.

Git branchDeployment
main, master, productionThe production App
stagingstaging.<app>
Any other named branchA preview whose label derives from a hash of the branch name, so it stays stable as the branch advances

A detached HEAD is rejected. The host’s port registry gives each App its loopback port; the first App, normally the administration App, serves the host’s own name.

App names

The App name defaults to package.json#name, and --name overrides it. A name has at most 32 characters, starts with a lowercase letter, ends with a letter or digit, and contains only lowercase letters, digits and dashes. Use the base name the token covers. A scoped npm package name needs an explicit --name. The names admin and host are refused in token mode.

Previews

Previews are private by default. The operator supplies their access link; opening the URL without its grant returns 403. Each deployment rotates that grant, so ask for the new link after an update. The deployment token and the preview grant serve separate purposes.

Replacement and recovery

Every arrival gets a named version. Before replacing a running App, the host records its image ID. If the new version does not answer HTTP, the host attempts to restore that image, together with the last environment revision proven to run with it, and still reports the deployment as failed. A successful deployment retains the newest three named versions.

  • Rollback restores an image. It never rolls back App data or database migrations.
  • A failed rollback is recorded and needs an operator.
  • After a confirmed rollback, fix the cause and submit a new deployment.
  • SSH and token deployments, lifecycle actions and preview retirement share one deployment lock.

SSH mode

An operator can deploy with an SSH identity instead of a token. The two credentials are mutually exclusive.

Terminal
pnpm exec kit deploy ajo-ops@panel.example.com --key /path/to/ssh-key

The host’s SSH identity must already be trusted; the script never accepts a new host key. The OCI archive is loaded directly, so no remote image registry is involved. The SSH path is the operator’s trusted path and keeps the platform capabilities that token deployment lacks. --resume works only with a token.

The direct helper

The package ships the SSH transport as deploy.sh, which can also be run by hand:

Terminal
sh deploy.sh <archive.oci> <ssh-key> <user@host> <app-name> [--ops | --env <label>]

--ops grants the typed host-operations capability and is intended for the administration App. It cannot be combined with an environment, and kit deploy does not expose it. Give it only to trusted platform applications.

Source of truth: ajo-server packages/ajo-kit-server/README.md