ox Docs
Log in

Config reference

The deploy config is a file named ox.toml at your repo root. Commit it, push, press Deploy. Every deploy validates it first: a missing or invalid file halts the deployment before anything on your server changes, with the exact reason shown on the run.

Unknown keys are errors. Secrets never live in this file; use the project's Environment editor.

runtime = "python"          # executable|python|node|bun|go|rust
package_manager = "uv"     # ""|uv|pip|npm|pnpm|yarn|bun
port = 9010                # 1-65535
required_packages = ["npm"]
writable_paths = ["/srv/ox/myapp/media"]

[[processes]]
name = "web"
command = "uv run gunicorn config.wsgi:application --port {port}"

[deploy]
install = ["uv", "sync", "--frozen"]
build = ["uv", "run", "python", "manage.py", "collectstatic", "--noinput"]
migrate = ["uv", "run", "python", "manage.py", "migrate", "--noinput"]
health_url = "http://127.0.0.1:9010/health"

Validate locally

Run ox validate [path] (default ./ox.toml) on your local machine before pushing. It runs the exact same strict validation checks that the deploy gate applies, catching syntax errors and unknown fields locally instead of halting a production deployment. Like nginx -t: exit 0 means valid; exit 1 prints the error with line number and reason.

Validate your deployment config

Run ox validate at your repository root to verify your ox.toml:

$ ox validate
ox.toml: manifest is valid

$ ox validate
ox validate: ox.toml: unknown project field typo_key

Services from the catalog

Postgres, Redis, MySQL, MongoDB, Neo4j, RabbitMQ, Memcached, ClickHouse, Qdrant, Meilisearch, NATS, Mailpit, LiveKit, Celery (+ beat), Flower, Prometheus, Ollama, SQLite tooling — every service is one [[services]] block. The catalog entry owns the mechanism: packages, apt sources, units, config files, credentials, ports. Each entry carries its tested_on matrix — the Ubuntu releases a real verification run covered on a disposable VPS — and the exact script that ran. Browse the services catalog or run ox catalog list / ox catalog show <id>.

[[services]]
id = "postgres@17"          # pinned variant; a bare name picks the default

[services.args]
extensions = ["vector"]

[[services]]
id = "celery@5"
[services.args]
app = "config"
workers = 4
queues = ["default", "mail"]

Generated credentials and URL keys (DATABASE_URL stays env-driven, MYSQL_URL, AMQP_URL, NEO4J_PASSWORD, QDRANT_URL, ...) land in the project's environment automatically — never pin them in the dashboard editor or the repo's .env files. Host-shared services keep their fixed ports and slice per-project resources; per-project services run as sandboxed units with ports allocated from 9100 upward.

Models from Hugging Face

Declare the models your app needs; every deploy pulls them into the project's model cache, authorized by the Hugging Face token you paste once in Settings (gated models fail with the settings fix when no token is set). The environment gains HF_HOME, so from_pretrained("BAAI/bge-m3") and friends resolve from the cache without path knowledge.

[[hf-models]]
id = "BAAI/bge-m3"
revision = "main"          # optional pin (branch, tag, or sha)

Writable paths are project-relative in the same spirit: writable_paths = ["media"] resolves against the project's base directory — host internals never need to appear in a manifest.

Deploy examples by project type

Twenty-one public reference apps, each tested working end-to-end with domain TLS. Pick the closest match to your stack, copy its ox.toml, swap in your values. Each repo has a walkthrough README and a .env.example.

Every app prints hello world <project>_<GREETING_TAG> from backend runtime env and frontend build-time env, so env handling is verifiable on every deploy. Workers and cron jobs print it to the journal.

Agent instructions

The Copy skill for AI agent button at the top copies the bundled ox.toml skill below for your coding agent. It is the full authoring contract: schema, rules, and examples.

Top-level keys

  • name (optional here): the project name. Omit it and the connected project's name is used. If you set it, it must match the project name.
  • source, branch: ignored; the project's clone URL and the deployed branch come from the project.
  • runtime, package_manager: how the app is built and run.
  • port: the port your app binds on 127.0.0.1 (behind nginx). Required; {port} substitutes into commands.
  • required_packages: apt-resolvable names, optionally pinned name=version.
  • writable_paths: extra ReadWritePaths for the app's systemd units.

[[processes]]

One systemd service per entry. At least one is required.

  • name: lowercase, alphanumeric start and end. Units are named ox-<project>-<name>.service.
  • command: the ExecStart. {port} resolves to this process's port.
  • port: per-process port override.
  • depends_on: other process names or [[services]] instance names (rendered as After= behind the service's unit). Unknown references fail validation.
  • pre_start: ExecStartPre lines run before the command.
  • readiness: http(s):// or tcp://host:port probe; ox waits for it before continuing.
  • restart_policy: systemd Restart= value; default on-failure.
  • memory_high_mb, memory_max_mb, cpu_percent: per-process resource overrides.

[limits]

  • memory_high_mb, memory_max_mb: cgroup memory limits.
  • cpu_percent: CPUQuota; 100 is one full core.
  • tasks_max, open_files: process count and fd limits.

[deploy]

  • install: dependency install commands, run in the new release before switching.
  • build: build commands run after install.
  • migrate: migration commands; the database is snapshotted before these run.
  • cleanup: commands run once when the project is deleted from the dashboard, as the project user in the current release. The author's uninstaller for anything install put outside the release (global tools, curl | bash); a failure is reported in the delete dialog and never blocks the delete.
  • health_url: checked after activation; a failing check fails the deploy.

Deleting a project

The delete dialog lists what the project owns on this server; each row is kept unless you uncheck it. Units, the registered manifest, and [deploy].cleanup always go.

  • Database: dumped, then dropped unless another registered project claims it; a kept database is remembered under /var/lib/ox/kept-databases/<db>.
  • Packages: required_packages are removed with apt only when no other registered project requires them and they are not host primitives (git, nginx, postgresql, ufw, lynis, lnav, certbot, python3, sqlite3). A dry run refuses the removal when apt would also take something else; nothing is ever autoremoved or purged. A uv project removes the curl-installed binary from /usr/local/bin.
  • Env file, secrets, releases, nginx site: removed when unchecked.

[frontend] and [[frontend.static]]

  • dist: built SPA assets directory, relative to the release root.
  • api_paths: URL prefixes that stay proxied to the app.
  • [[frontend.static]]: prefix + dist mounts a release directory at a URL prefix with hard 404s.

[[domains]]

  • name: the domain nginx serves.
  • tls: request a Let's Encrypt certificate.
  • spa: serve [frontend].dist statically with try_files; requires [frontend].dist.
  • process: which declared process's port this domain proxies to.
  • websockets, rate_limit: proxy tuning.

Postgres databases and extensions

  • The env names the database with DATABASE_URL alone: left empty, ox provisions one named after the project. Split DATABASE_* keys are the app's own ordinary env and ox never reads or writes them.
  • Extensions come from the postgres service's args (extensions = ["vector"]) and are created on the env-named database; their apt packages install automatically.
  • [database].stale_policy: backup-then-drop (default) or keep for databases left behind on destroy. A database kept at delete time is remembered under /var/lib/ox/kept-databases/<db> until the marker is removed.

[[cron_jobs]]

  • name, schedule (strict 5-field crontab, no macros), command: rendered as a oneshot service plus timer.

[[apt_sources]]

  • External repos (PGDG, NodeSource) rendered as deb822 sources before package install. Keys: name, url (https only), suite, components, key_url.

Environment variables (.env handling guide)

Managing environment variables and secrets safely between git, ox.toml, and your server:

  • Never put environment variables in ox.toml: ox.toml strictly validates keys. Inline [environment] blocks or raw env key-values are rejected at deploy validation.
  • Use .env.example with placeholders in git: When generating or committing repo files, agents and operators should commit a .env.example (or .env.sample) containing variable keys with dummy/placeholder values (e.g. DATABASE_URL=postgres://..., STRIPE_SECRET_KEY=, DEBUG=false). Never hardcode real credentials or secrets into git.
  • Set actual secrets in the Dashboard Environment Editor: Real secrets and production values belong exclusively in the project's Environment editor in the ox dashboard. They are written to the project's env file on the server (/srv/ox/<name>/env, mode 0600), merged at deploy, and redacted from deploy logs.
  • Merge priority at deploy time:
    1. Deterministic defaults (e.g. PORT matching your process port, NODE_ENV=production)
    2. Repository templates (.env.example, .env.sample)
    3. Existing server file (keys already present in the project's env file on disk are preserved)
    4. Dashboard Environment editor (highest priority; overrides earlier values)
  • Automatic infrastructure autowiring: If the merged environment references PostgreSQL (e.g. DATABASE_URL, POSTGRES_*) or Redis (REDIS_URL), ox automatically provisions and runs those native services on the host.
  • The env file's home: the final merged env always lands at /srv/ox/<name>/env (mode 0600, root-owned). The path is derived from the project name; there is no setting for it, and a leftover environment_file key is rejected like any unknown field.

[secrets]

Credential file names (values provided on the server out of band); units load them through systemd LoadCredential=. Names only, never values.