The default installation needs no options. Use this page to choose the sandbox backend at install time, run behind an existing reverse proxy, split Core and Web across hosts, run Core natively or install without internet access.
Pass options to the downloaded script:
./install.sh --sandbox dockerWith the one-line command, append them after bash -s --. The release downloader also accepts --version TAG to select a published release; otherwise it selects the latest stable release. It verifies the bundle's SHA-256 before extracting it and keeps the verified bundle for repair.
The installer prints each stage, then a summary of addresses, sign-in details and next steps. Set NO_COLOR=1 to disable colors. A failed step stops installation without a success message.
Docker Compose and hosting platforms
Use the self-contained Compose template with Docker Compose 2.26 or newer on Linux amd64. It pulls the existing, digest-pinned v0.0.3 images and starts PostgreSQL, Core, Web and an HTTP gateway. The one-time initialization service generates random secrets in persistent volumes and prepares the node installer; the migration service initializes the database before Core starts. Compose configuration owns the settings and volumes.
For a local trial, download compose.yaml and the local port override into one directory, then run:
docker compose -f compose.yaml -f local.yaml up -d --wait --wait-timeout 900
docker compose -f compose.yaml run --rm credentialsThe credentials command prints the generated Core key to your terminal without storing it in container logs. Open http://localhost:8080 and use that key to sign in. All installation secrets are generated automatically; keep the same Compose project and its volumes when restarting.
The first initialization downloads and verifies the release's approximately 385 MB control archive, retaining only the small node installation metadata. Later starts verify the saved files without downloading again. Image downloads are additional. An interrupted first initialization can be rerun; an existing database with missing installation secrets is refused.
You can deploy before choosing a domain: leave OAC_PUBLIC_URL unset or empty, then follow Compose configuration to set it and redeploy once the platform's domain is ready. The initial localhost origin allows services to start; Web accepts the configured host only, so platform-domain access becomes available after that redeployment.
Dokploy
Create a Docker Compose application and paste compose.yaml. Set OAC_PUBLIC_URL to the public HTTPS origin, enable isolated deployment, and add a domain for service gateway, port 8080. Enable HTTPS and select a certificate provider such as Let's Encrypt for that domain before deploying. Deploy without local.yaml; internal services publish no host ports. The template metadata supplies the generated domain and environment when packaging this Compose file for Dokploy's template catalog; HTTPS and its certificate provider still need to be enabled after import.
Coolify
Create a Docker Compose Empty service and paste compose.yaml. Set OAC_PUBLIC_URL to the public HTTPS origin and assign that domain to gateway on port 8080. Add Coolify's exclude_from_hc: true to the init, migrate and credentials service definitions so completed initialization and optional tooling do not affect its overall health. Save and deploy without local.yaml; Coolify supplies HTTPS.
On either platform, open its server terminal and run docker compose ls to find the deployed project name and Compose file. Using those exact values and the deployment's OAC_PUBLIC_URL, run docker compose -p <project-name> -f <compose-file> run --rm credentials, then sign in at the configured origin. The Dokploy domain guide and Coolify Compose guide describe their domain and service controls. These are importable deployment files; no hosted marketplace listing is published by this repository.
After signing in, choose the sandbox backend and add nodes using Nodes. The Compose stack deploys the control plane; execution machines remain separate.
Stop with docker compose stop using the same files and environment. Back up all installation volumes together while the services are stopped. Follow the installation version policy: a different release needs a new Compose project and fresh volumes.
Process settings
These flags seed the installation's config.json once. Their defaults, valid values and restart behavior are defined in the configuration reference. After installation, edit that file and run oac apply; rerunning the installer only repairs the installation.
| Flag | config.json field |
|---|---|
--core-only or --web-only | mode |
--native-core | native_core |
--public-url | public_url |
--host | host |
--core-port | ports.core |
--web-port | ports.web |
--core-url | web.core_url |
--ingress | ingress |
| //: # (END install-flags) |
--config FILE seeds config.json from a JSON file instead of these setting flags; they cannot be combined. A --config document follows the schema defaults, so set ingress: "managed" and host: "0.0.0.0" in it for managed HTTPS.
Installation actions
These options choose an installation location or perform initial setup; they are not saved in config.json.
| Option | Purpose |
|---|---|
--install-dir DIR | Absolute installation directory; defaults to ~/.oac/core. A new installation requires an empty or missing directory, or one holding an installation that never started |
--sandbox docker|microsandbox|e2b|none | Select the initial sandbox backend, saved in Core's database; change it later in Web |
--accept-docker-risks | Accept Docker's weaker isolation without an interactive prompt |
--e2b-api-key-file FILE | With E2B: absolute path to a private key file, no group/other access and at most 4 KiB |
--e2b-template ID:BUILD | With E2B: ready template build as template-id:build-uuid |
--e2b-api-url URL | With E2B: compatible service HTTPS API origin; use together with --e2b-domain |
--e2b-domain DOMAIN | With E2B: sandbox data-plane DNS suffix; use together with --e2b-api-url |
--core-key-file FILE | With Web-only: absolute path to a private file containing the existing Core key, at least 32 characters |
Several installations can share a machine when they use distinct installation directories and ports. Only one installation with managed HTTPS can hold ports 80 and 443 on an IP address; use distinct IP addresses or an external shared proxy for more. Each installation has its own database, Core key and nodes.
Sandbox backend
An installation runs its sandboxes on exactly one backend:
--sandbox | Sandboxes run on | You then |
|---|---|---|
microsandbox (default) | microVMs on your nodes, which need KVM | Add nodes in Web |
docker | Docker containers on your nodes; weaker isolation | Add nodes in Web |
e2b | E2B's cloud, sized by your template build | Nothing: E2B needs no nodes |
none | Nothing yet | Choose in Web under System → Manage sandbox configuration |
Docker and microsandbox start at the Standard size in Web's standard-sizes.json. If Core refuses the choice, for example because E2B rejects the key, the installer prints Core's message and exits; the services keep running and you choose the backend in Web. To change the backend or size later, see change the sandbox configuration.
Docker shares each node's kernel with its sandboxes, and its node service account is root-equivalent. Choose it only for trusted workloads or hosts without KVM. The installer asks for confirmation (default No); without a terminal, pass --accept-docker-risks.
E2B needs a public HTTPS URL that is not loopback, because E2B's sandboxes call Core from E2B's cloud, so pass --public-url. Prepare the template build with the E2B guide, then:
./install.sh --public-url https://core.example --sandbox e2b \
--e2b-api-key-file "$HOME/.oac/e2b-api-key" --e2b-template '<template-id>:<build-uuid>'Listeners and access
The default combined Docker installation selects --ingress managed and --host 0.0.0.0. Its gateway publishes Web on --web-port (8080 by default), and ports 80 and 443 once HTTPS is on. Core's --core-port stays on loopback and PostgreSQL stays private. --host accepts IPv4 or IPv6, without a port, scheme or zone. Use a concrete server IP in the browser, not a wildcard. Managed ingress needs a local Docker Unix socket.
--ingress external uses your own reverse proxy instead. It is the only choice for Core-only, Web-only and native Core installations, and their default. Core and Web then listen on --host, loopback by default. External non-loopback listeners require an HTTPS public_url and a reverse proxy, and Web's domain setup is unavailable: set public_url in config.json and run oac apply.
--public-url seeds a DNS-based HTTPS origin for unattended setup; with managed ingress, the certificate and connectivity checks must pass. The ingress mode is fixed for an installation.
Ports
Before it verifies the bundle or loads images, the installer checks --host and every port the installation will listen on: Web's and Core's, PostgreSQL's with native Core, and 80 and 443 with managed ingress and --public-url.
--hostmust be an address of this machine, or a wildcard such as0.0.0.0.- A port set with
--web-port,--core-portor in the--configfile must be free, and so must a port that a loopback--public-urlnames, such as 8080 inhttp://localhost:8080. Otherwise the installer stops, names the port and prints thesscommand that finds the program holding it. - A Web or Core port you leave out moves to the first free port above its default, at most 20 above, and never to another port of the same installation. The installer writes the chosen port to
config.jsonand names it in the summary, for examplePort 8080 was in use; Web uses 8081. - Managed ingress uses ports 80 and 443 only for HTTPS and never moves them. The gateway publishes them once
public_urlis set, from--public-urlor domain setup in Web, and no other program on the host may use them. If either is in use at installation, free it, install without--public-urland set up the domain later, or install with--ingress externaland use your own reverse proxy.
After installation, oac apply checks the ports of a changed host or port, and 80 and 443 when public_url turns HTTPS on. Domain setup in Web and oac domain check, before they start, that the hostname resolves and that no other program holds port 80 or 443, and name the port that is in use.
HTTPS and the reverse proxy
With external ingress, Core and Web share one public origin. Your reverse proxy terminates TLS and routes by path:
| Path | Goes to | Callers |
|---|---|---|
/v1, /v1/* | Core, 127.0.0.1:8091 by default | Applications, with a Project API key |
/api/v1/* | Core, 127.0.0.1:8091 | Nodes, sandboxes and self-hosted machines. Uses WebSockets |
| Everything else | Web, 127.0.0.1:8080 by default | Browsers, and node installers at /node-install/* |
The proxy must:
- Preserve Host. Web accepts only the host of its public URL.
- Pass WebSocket upgrades on
/api/v1. - Not buffer or time out streams.
/v1streams Session events. - Accept large uploads. Source files may reach 512 MiB; Core enforces the limits.
Run the proxy on the Core host while Core and Web listen on loopback, the default. oac status prints these routes with your addresses and ports.
Caddy obtains the certificate itself and passes Host and WebSockets by default:
core.example {
@core path /v1 /v1/* /api/v1/*
handle @core {
reverse_proxy 127.0.0.1:8091
}
handle {
reverse_proxy 127.0.0.1:8080
}
}nginx, for example in /etc/nginx/conf.d/oac.conf inside the http block:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name core.example;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name core.example;
ssl_certificate /etc/letsencrypt/live/core.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/core.example/privkey.pem;
client_max_body_size 0; # Core enforces its own upload limits
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off; # server-sent events on /v1
proxy_request_buffering off;
proxy_read_timeout 1h; # long-lived WebSockets and streams
proxy_send_timeout 1h;
location = /v1 { proxy_pass http://127.0.0.1:8091; }
location /v1/ { proxy_pass http://127.0.0.1:8091; }
location /api/v1/ { proxy_pass http://127.0.0.1:8091; }
location / { proxy_pass http://127.0.0.1:8080; }
}Then set public_url in ~/.oac/core/config.json and run ~/.oac/core/oac apply. Check the routing:
curl -s -o /dev/null -w '%{http_code}\n' -H 'OpenAI-Beta: agents=v1' https://core.example/v1/agents401 means /v1 reached Core, which asks for a key. 404 means it reached Web: fix the proxy, or application calls and every node connection will fail.
TLS verification stays on everywhere. With a private certificate authority, node hosts, self-hosted machines and the Runtime image must trust it.
Try it locally with a quick tunnel
A Cloudflare quick tunnel gives a trial installation with external ingress a temporary public HTTPS address. It forwards to one port, so put a local proxy with the same routes in front:
http://:8443 {
bind 127.0.0.1
@core path /v1 /v1/* /api/v1/*
handle @core {
reverse_proxy 127.0.0.1:8091
}
handle {
reverse_proxy 127.0.0.1:8080
}
}- Start the proxy:
caddy run --config Caddyfile. - Start the tunnel:
cloudflared tunnel --url http://127.0.0.1:8443. It prints an address such ashttps://random-words.trycloudflare.com. - Set that address as
public_urlin~/.oac/core/config.jsonand run~/.oac/core/oac apply.
The address changes whenever cloudflared restarts; nodes bound to the old address must then be added again. Throughput is low, so a node's first Runtime download (about 500 MB) can be slow; see slow links.
Modes
| Mode | Runs | Use it for |
|---|---|---|
| all (default) | PostgreSQL, Core and Web | Most installations |
--core-only | PostgreSQL and Core | A Core whose Web runs elsewhere, or scripts only. No Add node command |
--web-only | Web | A second host for the console, paired with an existing Core |
The mode and native Core are fixed once installed; to change them, install into a new directory.
A Web-only console forwards signed-in /core/v1 requests to its Core with the Core key, so --core-url must reach Core's /core/v1 directly. The public URL doesn't, because the proxy sends /core/v1 to Web:
- Web on the Core host: use
--core-url http://127.0.0.1:8091. - Web on another host: give Core a second HTTPS name that sends every path to Core, such as
https://core-api.example, and allow only the Web host's address.
Split deployment
| Host | Install | Reverse proxy |
|---|---|---|
| Core | ./install.sh --core-only --public-url https://core.example | core.example: /v1, /v1/* and /api/v1/* to Core; /node-install/* to the Web host with Host rewritten to console.example; nothing else. core-api.example: every path to Core, for the Web host's address only |
| Web | ./install.sh --web-only …, below | console.example: every path to Web |
./install.sh --web-only --install-dir "$HOME/.oac/web" \
--public-url https://console.example \
--core-url https://core-api.example \
--core-key-file "$HOME/core.key"Browsers and node installers use console.example. Applications, nodes, sandboxes and self-hosted machines call Core at core.example; self-hosted machines also download their installer there, which is why core.example forwards /node-install/* to Web. Missing node files redirect to the release; for disconnected nodes, install Web from the offline bundle.
Caddy on the Core host, where 203.0.113.10 is the Web host's address:
core.example {
@core path /v1 /v1/* /api/v1/*
handle @core {
reverse_proxy 127.0.0.1:8091
}
handle /node-install/* {
reverse_proxy https://console.example {
header_up Host console.example
}
}
handle {
respond 404
}
}
core-api.example {
@web remote_ip 203.0.113.10
handle @web {
reverse_proxy 127.0.0.1:8091
}
handle {
respond 403
}
}nginx on the Core host, with the map from the main example:
server {
listen 443 ssl;
server_name core.example;
ssl_certificate /etc/letsencrypt/live/core.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/core.example/privkey.pem;
client_max_body_size 0;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
location = /v1 { proxy_pass http://127.0.0.1:8091; }
location /v1/ { proxy_pass http://127.0.0.1:8091; }
location /api/v1/ { proxy_pass http://127.0.0.1:8091; }
location /node-install/ {
proxy_pass https://console.example;
proxy_set_header Host console.example; # Web serves node files only for its own name
proxy_ssl_server_name on;
proxy_ssl_name console.example;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
}
location / { return 404; }
}
server {
listen 443 ssl;
server_name core-api.example;
ssl_certificate /etc/letsencrypt/live/core-api.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/core-api.example/privkey.pem;
allow 203.0.113.10; # the Web host
deny all;
client_max_body_size 0;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 1h;
location / { proxy_pass http://127.0.0.1:8091; }
}The Web host's proxy sends every path to Web, for example console.example { reverse_proxy 127.0.0.1:8080 } in Caddy.
Copy secrets/core.key from the Core host with mode 0600, then delete $HOME/core.key; the installer keeps its own copy. After a Core key rotation, copy it again. A Web-only install selects no sandbox backend.
Native Core
--native-core runs Core as a systemd user service; PostgreSQL and Web stay in containers. It needs a running systemd user manager with lingering, which the host administrator enables with sudo loginctl enable-linger "$USER", and the bundle's native binaries must load on the host. PostgreSQL then listens on a loopback port the installer picks (ports.database). Native Core is unrelated to the sandbox backend.
Offline hosts
Transfer the release's *-linux-amd64-offline.tar.gz and its .sha256 file, verify and extract them, then run the bundled ./install.sh. The offline bundle also carries the node and Runtime files, so Web serves them to nodes without release access.