Begin
Deploy your first App
From an empty directory to a live App over HTTPS, on a local host or on DigitalOcean.
This page takes the starter from an empty directory to a live App over HTTPS, on a host of your own. Choose where the host runs: a local host on this machine, which needs no cloud account, or a DigitalOcean Droplet with a public name. Both run the same signed HostImage release with its administration App, and in both the App requests its certificate during its first deploy.
What you need
Node 22.18 or newer, pnpm and Git, on Linux x64: kit deploy seals the App with ajo-engine-compiler, which runs only there. On macOS or Windows the command stops with platform_unsupported, and WSL has not been verified; deploy from a Linux x64 machine or a CI runner instead. The rest depends on the host:
| Requirement | Local host | DigitalOcean |
|---|---|---|
| This machine | Linux x86_64 with KVM, and read and write access to /dev/kvm | Linux x64 |
| Tools | qemu-system-x86_64, qemu-img, bzip2 and the OpenSSH client | doctl, DigitalOcean’s CLI, and the OpenSSH client |
| Once per machine | Two root commands for ports 80 and 443, and trusting the local root in each browser | doctl auth init |
| For each host | Nothing | Two DNS records at your DNS provider |
| Cost | None | The Droplet and its custom image, until you delete the host |
On Debian and Ubuntu, the local host’s tools come from the packages qemu-system-x86, qemu-utils and bzip2. A local host runs only on Linux x86_64 with KVM; on any other system kit host create stops with local_unsupported.
Create the App
pnpm create ajo notes
cd notesThe Quick start describes what this creates. The starter lists ajo-kit-server, which adds the kit host and kit deploy commands, and the engine pair as optional dependencies. Its Git repository is on main, so it deploys to production; no commit is needed first.
A local host
A local host is the HostImage in QEMU on this machine, with no outbound network. Its name ends in .localhost, which browsers resolve to this machine: the administration App answers at https://notes.localhost and each App at https://<app>.notes.localhost.
Once per machine: ports 80 and 443
The host listens on loopback ports 80 and 443. Linux reserves ports below 1024 for root, so lower that limit once, as root:
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/ajo.conf
sudo sysctl -p /etc/sysctl.d/ajo.confAfter it, any user of this machine can listen on ports 80 to 1023. kit host create never runs these commands itself: when a port needs root or is in use, it stops with privileged_ports and prints them.
Create the host
pnpm kit host create --provider local --name notes.localhostThe command downloads the latest HostImage release and verifies its signature, keeps the image in a cache this machine’s local hosts share, and boots it with KVM, 1 GiB of memory and a 20 GiB disk. It waits, at most 10 minutes, until the host serves its name over HTTPS and its administration App is running. Then it sets kit.host in the App’s package.json:
○ Download HostImage hostimage-0123456789abcdef
○ Boot notes.localhost from hostimage-0123456789abcdef
○ Waiting for https://notes.localhost
○ Pinned the SSH key SHA256:...
○ Waiting for the admin
✓ notes.localhost is ready: https://notes.localhost
Claim the admin (single use, 24 hours): https://notes.localhost/claim/...
Trust the local root once in each browser: import ~/.config/ajo/ca/root.pem as a certificate authority for websites
(Chrome: chrome://certificate-manager; Firefox: about:preferences#privacy, View Certificates, Authorities)
○ package.json#kit.host is notes.localhostThe host’s certificates come from a certificate authority on this machine, never from the internet. Its root is made once per machine in ~/.config/ajo/ca/ and can sign only localhost, the names under it and the host’s metadata address 169.254.169.254, so trusting it trusts no other site. Import root.pem in each browser you will use, as the command says; kit never installs it anywhere.
- One local host runs per machine, because it takes ports 80 and 443.
- QEMU keeps running after the command ends. After this machine restarts, run the same command again from the App: it boots the host again. After removing or upgrading the App that created the host, the host can no longer renew its certificates, which live seven days, and nothing says so: once QEMU has stopped, boot it again with the same command, or delete the host and create it again.
- Everything happens on this machine, so a local host does not exercise outbound mail or off-host backups.
A DigitalOcean host
A DigitalOcean host is a Droplet made from the HostImage. Its name is a public host name whose DNS you control, such as apps.example.com: the administration App answers at https://apps.example.com and each App at https://<app>.apps.example.com. Install doctl, create an API token with read and write access in DigitalOcean’s control panel, and give it to doctl once:
doctl auth initThen create the host, with your own name:
pnpm kit host create --provider digitalocean --name apps.example.comThe command imports the release’s image as a custom image in your account, creates an s-1vcpu-1gb Droplet from it in nyc3 (--region chooses another) and prints two DNS records:
apps.example.com A 203.0.113.7
*.apps.example.com A 203.0.113.7Create both at your DNS provider. The command waits, at most 10 minutes, until they resolve; if they do not, it ends with the state waiting_dns, and running the same command again continues where it stopped. The host then requests its certificate from Let’s Encrypt by itself, and the command waits for its HTTPS and its administration App, and sets kit.host in the App’s package.json.
Deploy
pnpm kit deployOne command builds and seals the App, writes its image, sends it to the host in kit.host over the SSH access that kit host create recorded, and waits for the host’s outcome. The App’s name is package.json#name:
○ Branch main: production (notes)
○ Build and seal
○ Package notes
○ Send to notes.localhost
✓ notes is live: https://notes.notes.localhostThe App is live with its own certificate, and plain HTTP redirects to HTTPS. When the certificate is not ready yet, the deploy still succeeds and says HTTPS pending <reason>: the App answers over HTTP, and the host keeps requesting the certificate by itself, so there is nothing to deploy again. To ship a change, run pnpm kit deploy again; another branch deploys staging or a private preview (see Targets).
Every command takes --json, and every failure names a stable code and the next step. The starter needs no secrets to go live: the host supplies APP_URL and generates APP_SECRET.
Claim your host
The claim link that kit host create printed makes you the host’s owner. Open it in a browser (on a local host, after trusting the root) and register a passkey. It works once, for 24 hours, and it is an owner credential: keep it out of files, logs and commits. In the administration App you manage the host’s Apps, their secrets and domains, and the tokens that let other machines deploy.
Turn on mail
The starter goes live with mail off: accounts and notes work, and where the App would send mail it says that mail is not set up yet. To send mail, open the App’s Settings in the administration App, then Secrets, and set all three of MAIL_FROM, MAIL_URL and MAIL_TOKEN for your email provider. The host replaces the running App with one that has them; no deploy is needed. A local host has no outbound network, so its Apps cannot deliver mail.
- Deploy an AppTargets, tokens, recovery, and updating or deleting a host.
- The hostDomains, secrets, backups and the privilege model.
- Ajo ServerWhat a host includes and where it stands.
Full reference: ajo-kit-server on npm