Skip to documentation

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:

The hostname or IP you choose becomes PLATFORM_BASE_DOMAIN.

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:

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:

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:

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

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.