Production & bootstrap
This guide puts Ax on a server you control. A Hetzner, Fly machine, bare VM, or cloud instance is fine as long as it can run Docker and receive traffic on ports 80 and 443.
Before you start
You need:
- An Ubuntu server or similar Linux host.
- Docker and the Compose plugin installed.
- Ports
80and443open to the internet. - A hostname such as
apps.example.com, or a public IP if you are not using DNS. - The
axCLI installed on your laptop.
The hostname or IP you choose becomes PLATFORM_BASE_DOMAIN.
Recommended bootstrap
1. Create a token on your laptop
Generate the runner token locally:
ax generate
Keep this token. The same value must be used by the server and your CLI.
2. Clone and run setup on the server
git clone https://github.com/narnia-sh/ax.git
cd ax
./setup.sh
When prompted, enter:
PLATFORM_BASE_DOMAIN, for exampleapps.example.comor203.0.113.10.RUNNER_TOKEN, using the token fromax generate.
The setup script writes infra/.env with mode 600 and starts the stack with Docker Compose.
3. Point DNS at the server
If PLATFORM_BASE_DOMAIN is a hostname, create A/AAAA records pointing it at the server IP.
That one host serves:
/_ax/healthfor platform liveness./v1/...and/healthfor the runner API.- App traffic, such as
/myapi/..., depending onax.toml.
If you use a raw public IP, skip DNS. The CLI will use plain HTTP for IP-based stacks.
4. Login from your laptop
Login with the same hostname or IP you set as PLATFORM_BASE_DOMAIN:
ax login apps.example.com
When you omit --token, the CLI reads the token created by ax generate from ~/.config/ax/runner-token.
Examples:
PLATFORM_BASE_DOMAIN=apps.example.com->ax login apps.example.comPLATFORM_BASE_DOMAIN=203.0.113.10->ax login 203.0.113.10
5. Deploy apps
From each uv Python project:
ax init
ax deploy
ax ps
ax logs myapi --tail 300
Non-interactive setup
For cloud-init or automation, export values before running setup:
export PLATFORM_BASE_DOMAIN=apps.example.com
export RUNNER_TOKEN='<same token as ax generate>'
./setup.sh
You can also copy infra/.env.example to infra/.env, edit values, then start Compose directly:
docker compose -f infra/docker-compose.yml up --build -d
You can still login with an explicit token when needed:
ax login apps.example.com --token '<runner token>'
PLATFORM_BASE_DOMAIN (runner + apps host)
Set this in infra/.env or through ./setup.sh. It must be only the hostname or IP, without http:// or https://.
Correct:
PLATFORM_BASE_DOMAIN=apps.example.com
Incorrect:
PLATFORM_BASE_DOMAIN=https://apps.example.com
Real hostnames use TLS on :443. HTTP on :80 is also served for the same routes, which is useful before certificates are ready and for IP-only bases.
If infra/.env is missing, local defaults are used: localhost and local-dev-token.
Security defaults
- Expose only ports
80and443publicly. - Keep
RUNNER_TOKENsecret. - The runner container is not published directly on the host.
- Caddy reverse-proxies
/v1/*and/healthto the runner onPLATFORM_BASE_DOMAIN.
Upgrading an existing host
After pulling Ax changes, recycle the stack so Compose picks up service, image, and network changes:
docker compose -f infra/docker-compose.yml down
docker compose -f infra/docker-compose.yml up --build -d
If old containers still conflict, inspect them with docker ps -a and remove only the stale Ax containers that are blocking the new stack.