Single-tenant deploy platform for uv Python projects.
ax deploys Python apps as normal Linux processes instead of Docker containers:
runner/: FastAPI API server that receives deploys, creates per-release virtualenvs withuv, writes systemd units, and updates Caddy routes.cli/:axCLI to package local code and call the runner.infra/: Caddy config for the runner API and deployed app routes.
The default runtime is optimized for low overhead:
- one systemd service per deployed app
- one
.venvper app release, so dependency versions do not conflict - one shared
UV_CACHE_DIR, so downloads/builds are reused across apps - Caddy reverse-proxies to each app on an allocated localhost port
This is intended for a single trusted owner running their own apps. Keep Docker or another stronger isolation backend for untrusted multi-tenant code.
cd cli
uv sync
uv run ax --helpOr install the CLI globally:
uv tool install -e ./cliPrereqs on the server:
- Linux with systemd
uvcaddy- ports
80and443open - DNS for
PLATFORM_BASE_DOMAINpointing to the server, or use the server IP
Recommended token flow:
ax generateOn the server:
git clone <this repo>
cd ax
export PLATFORM_BASE_DOMAIN=apps.example.com
export RUNNER_TOKEN='<same token as ax generate>'
sudo -E ./setup.shsetup.sh writes:
infra/.envfor repo-local record ofPLATFORM_BASE_DOMAINandRUNNER_TOKEN/etc/ax/runner.envfor the runner service/etc/systemd/system/ax-runner.service/etc/systemd/system/caddy.service.d/ax.conf/etc/caddy/Caddyfile
It also creates:
/var/lib/ax/apps/var/cache/ax/uv/etc/caddy/apps
Then it starts/restarts caddy and ax-runner.
On your laptop:
ax login apps.example.com
ax health
ax init
ax deploy
ax ps
ax logs myapi --tail 300
ax restart myapi
ax stop myapi
ax start myapi
ax rm myapiFor localhost and raw IPs the CLI uses http://; for normal hostnames it uses https://.
ax.toml is the app manifest. It tells ax what to run, how to route it, and what resource limits to apply.
Top-level fields:
name = "myapi" # app name, becomes the systemd unit/app identity
type = "web" # metadata for now
start = "uvicorn app:app --host 127.0.0.1 --port $PORT"
port = 8000 # compatibility field; runtime allocates the actual localhost portSections:
[ingress] # how Caddy should expose the app
[runtime] # host-process runtime settings
[env] # environment variables injected into the serviceMinimal web app:
name = "myapi"
type = "web"
start = "uvicorn app:app --host 127.0.0.1 --port $PORT"
port = 8000
[ingress]
mode = "platform-path"
path = "/myapi"
[runtime]
backend = "process"
python = "3.12"
memory = "512M"
cpu = "1"
[env]
ENV = "prod"start should be the app command, not an environment-management command.
The runner starts it with the release venv active by setting VIRTUAL_ENV and putting .venv/bin first on PATH.
For web apps, bind to 127.0.0.1 and use $PORT.
port is kept for app compatibility, but the process runtime allocates a private localhost port per app and exposes it as $PORT.
The [runtime] section currently supports:
backend = "process": the only supported backend right nowpython = "3.12": interpreter version foruv syncmemory = "512M": systemdMemoryMaxcpu = "1": systemdCPUQuota, where1means one full core
Ingress modes:
[ingress]
mode = "platform-path"
path = "/myapi"[ingress]
mode = "platform-subdomain"
subdomain = "myapi"[ingress]
mode = "custom-domain"
domains = ["api.example.com"]For each deploy, the runner:
- extracts the source into
/var/lib/ax/apps/<name>/releases/<id>/src - runs
uv sync --project <src>usingUV_CACHE_DIR=/var/cache/ax/uv - atomically updates
/var/lib/ax/apps/<name>/current - writes
/etc/systemd/system/ax-<name>.service - restarts the app service
- writes Caddy route snippets under
/etc/caddy/apps - reloads Caddy
Each app gets an isolated virtualenv:
/var/lib/ax/apps/api-a/current/src/.venv
/var/lib/ax/apps/api-b/current/src/.venv
All apps share the cache:
/var/cache/ax/uv
This means conflicting dependency versions are fine across apps, while common packages are downloaded/built once.
Runner service:
systemctl status ax-runner
journalctl -u ax-runner -fApp service:
systemctl status ax-myapi
journalctl -u ax-myapi -fCaddy:
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
systemctl reload caddy