Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
| Path | Contents |
| --- | --- |
| `install.sh` | Host installer published with each release |
| `compose/` | Compose template, port overlays and their tests |
| `compose/` | Compose template and its tests |
| `distribution/` | Image Dockerfiles |
| `node/` | [Node installer](node/README.md), packaged as `node-install.pyz` |

## Installation

`install.sh` downloads its release's `compose.yaml` and port files, checks them against `compose-sha256sums.txt`, writes `.env`, and starts Compose. Core applies database migrations when it starts. The host needs Linux amd64 and Docker Compose 2.26 or newer. [Configuration](../docs/configuration.md) owns the installation layout and settings.
`install.sh` downloads its release's `compose.yaml`, checks it against `compose-sha256sums.txt`, writes `.env`, and starts Compose. Core applies database migrations when it starts. The host needs Linux amd64 and Docker Compose 2.26 or newer. [Configuration](../docs/configuration.md) owns the installation layout and settings.

`oac` is a Go command (`services/core/cmd/oac`) in the Core image and the ingress image. The host copy implements `apply`, `core-key` and `rotate-core-key`; `core-key --show` runs `oac-web core-key` in the Web container. Start, stop, logs and removal are `docker compose`. `apply` runs `oac-core check-config` before recreating services. The ingress image runs data initialization as `oac init` and contains no Python. No service receives a Docker socket.
`oac` is a Go command (`services/core/cmd/oac`) in the Core image and the ingress image. The host copy implements `apply`, `core-key` and `rotate-core-key`; `core-key --show` runs `oac-web core-key` in the Web container. Start, stop, logs and removal are `docker compose`. `apply` runs `oac-core check-config` before recreating services. The ingress image runs data initialization as `oac init`, verifies and copies its bundled node metadata without network access, and contains no Python. No service receives a Docker socket.

Web serves the console and forwards `/v1` and `/api/v1` to Core, so it is the only published service. HTTPS is terminated by the operator's reverse proxy or hosting platform, which routes to `web:8080`; `OAC_PUBLIC_URL` records that origin.

Expand Down
16 changes: 6 additions & 10 deletions deploy/compose/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Release template. scripts/render-compose.py fills the __OAC_*__ tokens with this
# release's source revision and node-metadata checksum. Images default to the
# floating latest tags; set OAC_IMAGE_CORE, OAC_IMAGE_WEB or OAC_IMAGE_INGRESS
# to select another reference. Do not run this file until it has been rendered.
# Data is bind-mounted from ${OAC_DATA_DIR:-./data}. Set OAC_PUBLIC_URL when the
# platform domain is ready; startup defaults to localhost. Other process
# settings pass through unchanged; Core owns their defaults.
x-ingress-image: &ingress-image ${OAC_IMAGE_INGRESS:-ghcr.io/minimax-ai/openagentcore/ingress:latest}
# Release template. The publisher pins the initialization image to this release.
# Core and Web default to latest; OAC_IMAGE_* selects another reference.
# Data is bind-mounted from ${OAC_DATA_DIR:-./data}.
x-ingress-image: &ingress-image ${OAC_IMAGE_INGRESS:-__OAC_INIT_IMAGE__}
services:
init:
image: *ingress-image
Expand All @@ -15,8 +11,6 @@ services:
command: [/usr/local/bin/oac, init]
environment:
OAC_REVISION: __OAC_REVISION__
OAC_RELEASE_BASE: __OAC_RELEASE_BASE__
OAC_ARCHIVE_CHECKSUM: __OAC_ARCHIVE_CHECKSUM__
volumes:
- type: bind
source: ${OAC_DATA_DIR:-./data}
Expand Down Expand Up @@ -89,6 +83,8 @@ services:
target: /state

web:
ports:
- "${OAC_HOST:-127.0.0.1}:${OAC_WEB_PORT:-8080}:8080"
image: ${OAC_IMAGE_WEB:-ghcr.io/minimax-ai/openagentcore/web:latest}
platform: linux/amd64
user: "65532:65532"
Expand Down
2 changes: 1 addition & 1 deletion deploy/compose/dokploy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ main_domain = "${domain}"
env = ["OAC_PUBLIC_URL=https://${main_domain}"]

[[config.domains]]
serviceName = "gateway"
serviceName = "web"
port = 8080
host = "${main_domain}"
6 changes: 0 additions & 6 deletions deploy/compose/ports.yaml

This file was deleted.

24 changes: 14 additions & 10 deletions deploy/compose/test_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
def rendered_compose(directory):
text = render_compose.render({
'REVISION': 'd' * 40,
'RELEASE_BASE': 'https://example.com/releases/v1/',
'ARCHIVE_CHECKSUM': 'e' * 64,
'INIT_IMAGE': 'ghcr.io/minimax-ai/openagentcore/ingress@sha256:' + 'e' * 64,
})
path = Path(directory) / 'compose.yaml'
path.write_text(text)
Expand All @@ -31,6 +30,8 @@ class ComposeTests(unittest.TestCase):
def render(cls, public_url=None):
env = dict(os.environ)
env.pop('OAC_PUBLIC_URL', None)
env.pop('OAC_HOST', None)
env.pop('OAC_WEB_PORT', None)
for name in ('OAC_IMAGE_CORE', 'OAC_IMAGE_WEB', 'OAC_IMAGE_INGRESS'):
env.pop(name, None)
env['OAC_DATA_DIR'] = '/tmp/oac-compose-fixture'
Expand All @@ -55,8 +56,9 @@ def test_compose_uses_private_services_and_ordered_initialization(self):
self.assertEqual(sorted(services), ['core', 'database', 'init', 'web'])
for service in services.values():
self.assertNotIn('build', service)
self.assertNotIn('ports', service)
self.assertTrue(service['image'].endswith(':latest') or service['image'] == 'postgres:16-alpine')
if service is not services['web']:
self.assertNotIn('ports', service)
self.assertTrue(service['image'].endswith(':latest') or service['image'] == 'postgres:16-alpine' or service['image'].endswith('@sha256:' + 'e' * 64))
for volume in service.get('volumes', []):
self.assertNotIn('docker.sock', json.dumps(volume))
self.assertEqual(volume['type'], 'bind')
Expand Down Expand Up @@ -84,13 +86,15 @@ def test_public_url_can_be_configured_after_initial_startup(self):
for service, spec in self.compose['services'].items()})

def test_host_ports_publish_only_web(self):
env = dict(os.environ, OAC_DATA_DIR='/tmp/oac-compose-fixture', OAC_HOST='0.0.0.0')
hosted = json.loads(subprocess.check_output(
def ports(config):
return {name: [(port.get('host_ip'), port['published']) for port in service.get('ports', [])]
for name, service in config['services'].items() if service.get('ports')}
self.assertEqual(ports(self.compose), {'web': [('127.0.0.1', '8080')]})
env = dict(os.environ, OAC_DATA_DIR='/tmp/oac-compose-fixture', OAC_HOST='0.0.0.0', OAC_WEB_PORT='9080')
configured = json.loads(subprocess.check_output(
['docker', 'compose', '--env-file', os.devnull, '-f', str(self.compose_file),
'-f', str(ROOT / 'deploy/compose/ports.yaml'), 'config', '--format', 'json'], env=env))
published = {name: [(port.get('host_ip'), port['published']) for port in service.get('ports', [])]
for name, service in hosted['services'].items() if service.get('ports')}
self.assertEqual(published, {'web': [('0.0.0.0', '8080')]})
'config', '--format', 'json'], env=env))
self.assertEqual(ports(configured), {'web': [('0.0.0.0', '9080')]})

def test_platform_network_injection_keeps_the_file_valid(self):
# Dokploy isolated deployments attach a project network to every service.
Expand Down
10 changes: 4 additions & 6 deletions deploy/distribution/Ingress.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# One-time data initialization. oac init runs as root so it can chown data
# directories, then exits. It downloads the node payload over HTTPS, so the
# image carries CA certificates. scripts/build-core-distribution.sh builds this
# from a context that also contains the oac binary.
FROM alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
RUN apk add --no-cache ca-certificates
# One-time data initialization runs as root to prepare data ownership.
# Only the node installation metadata accompanies the oac binary.
FROM scratch
COPY --chmod=0555 oac /usr/local/bin/oac
COPY --chmod=0444 node-payload/ /opt/oac/node-payload/
ENTRYPOINT []
15 changes: 10 additions & 5 deletions deploy/install.dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,15 @@ go_build() {
go_build services/core/cmd/environment-key "$build/core/bin/oac-core-environment-key"
go_build services/core/cmd/oac "$build/core/bin/oac"
go_build services/web "$build/web/oac-web"
go_build services/core/cmd/oac "$build/ingress/oac"
payload_revision="$(python3 - "$build/ingress/node-payload" <<'PYCODE'
import importlib.util, pathlib, sys
spec = importlib.util.spec_from_file_location("smoke", "scripts/compose-smoke.py")
smoke = importlib.util.module_from_spec(spec)
spec.loader.exec_module(smoke)
print(smoke.prepare_pinned_payload(pathlib.Path(sys.argv[1])))
PYCODE
)"
go build -trimpath -ldflags "-X main.buildRevision=$payload_revision" -o "$build/ingress/oac" ./services/core/cmd/oac
)
mkdir -p "$build/core/e2b" "$build/core/native-installers"
python3 - "$build/core/native-installers/catalog.json" "$revision" "$protocol" <<'PY'
Expand Down Expand Up @@ -100,16 +108,13 @@ spec.loader.exec_module(render)
pins = json.loads((root / "deploy/compose/smoke-pins.json").read_text())
(dest / "compose.yaml").write_text(render.render({
"REVISION": pins["revision"],
"RELEASE_BASE": pins["release_base"],
"ARCHIVE_CHECKSUM": pins["archive_checksum"],
"INIT_IMAGE": "ghcr.io/minimax-ai/openagentcore/ingress@sha256:" + "0" * 64,
}))
PY
cp "$repo_root/deploy/compose/ports.yaml" "$install_dir/ports.yaml"

umask 077
cat >"$install_dir/.env" <<EOF
COMPOSE_PROJECT_NAME=oac-local
COMPOSE_FILE=compose.yaml:ports.yaml
OAC_DATA_DIR=$install_dir/data
OAC_HOST=$host_address
OAC_WEB_PORT=$web_port
Expand Down
13 changes: 4 additions & 9 deletions deploy/install.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Install Core and Web from one release's Compose files. The host needs Docker.
# Install Core and Web from one release's Compose file. The host needs Docker.
set -euo pipefail

repository="${OAC_REPOSITORY:-MiniMax-AI/OpenAgentCore}"
Expand Down Expand Up @@ -81,7 +81,7 @@ cleanup() {
docker compose logs --no-color --tail 50 >&2 || true
docker compose down --remove-orphans
# Containers own data/; remove it from a container as well.
if [[ -d data ]]; then docker compose run --rm --no-deps --entrypoint find init /data -mindepth 1 -delete; fi
if [[ -d data ]]; then docker compose run --rm --no-deps --volume "$install_dir/data:/data" --entrypoint find database /data -mindepth 1 -delete; fi
) >/dev/null 2>&1 || true
rm -rf "$install_dir"
fi
Expand Down Expand Up @@ -110,18 +110,13 @@ if [[ -z "$public_url" ]]; then public_url="http://localhost:$web_port"; local_o

mkdir -p "$install_dir"
chmod 700 "$install_dir"
files=(compose.yaml ports.yaml)
curl --fail --silent --show-error --location "$asset_base/compose-sha256sums.txt" --output "$install_dir/compose-sha256sums.txt"
for name in "${files[@]}"; do
curl --fail --silent --show-error --location "$asset_base/$name" --output "$install_dir/$name"
done
(cd "$install_dir" && sha256sum --check --ignore-missing --quiet compose-sha256sums.txt)
curl --fail --silent --show-error --location "$asset_base/compose.yaml" --output "$install_dir/compose.yaml"
(cd "$install_dir" && sha256sum --check --quiet compose-sha256sums.txt)

compose_file="$(IFS=:; echo "${files[*]}")"
umask 077
{
echo "COMPOSE_PROJECT_NAME=oac-$(od -An -N5 -tx1 /dev/urandom | tr -d ' \n')"
echo "COMPOSE_FILE=$compose_file"
echo "OAC_INSTALL_DIR=$install_dir"
echo "OAC_HOST=$host_address"
echo "OAC_WEB_PORT=$web_port"
Expand Down
3 changes: 1 addition & 2 deletions deploy/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,8 @@ def test_env_holds_only_the_installation_choices(self):
completed, _ = self.install(root, "--public-url", "https://core.example")
self.assertEqual(completed.returncode, 0, completed.stderr)
env = dict(line.split("=", 1) for line in (root / "oac/.env").read_text().splitlines())
self.assertEqual(env["COMPOSE_FILE"], "compose.yaml:ports.yaml")
self.assertEqual(env["OAC_PUBLIC_URL"], "https://core.example")
self.assertEqual(sorted(env), ["COMPOSE_FILE", "COMPOSE_PROJECT_NAME", "OAC_HOST", "OAC_INSTALL_DIR", "OAC_PUBLIC_URL", "OAC_WEB_PORT"])
self.assertEqual(sorted(env), ["COMPOSE_PROJECT_NAME", "OAC_HOST", "OAC_INSTALL_DIR", "OAC_PUBLIC_URL", "OAC_WEB_PORT"])

def test_help_does_not_need_docker(self):
help_text = subprocess.run(["bash", str(INSTALL), "--help"], capture_output=True, text=True, check=True)
Expand Down
5 changes: 2 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ To change it, point the reverse proxy at the new address first, then edit `OAC_P
| Variable | Default | Meaning |
| --- | --- | --- |
| `OAC_PUBLIC_URL` | `http://localhost:8080` | Origin applications, nodes, sandboxes and self-hosted executors use. See [changing the public URL](#changing-the-public-url) |
| `OAC_HOST` | `127.0.0.1` | Address published by `ports.yaml`. `install.sh` sets `0.0.0.0` |
| `OAC_HOST` | `127.0.0.1` | Web bind address published by `compose.yaml`. `install.sh` sets `0.0.0.0` |
| `OAC_WEB_PORT` | `8080` | Host port of Web |
| `COMPOSE_FILE` | `compose.yaml:ports.yaml` | The Compose files. `ports.yaml` publishes Web; hosting platforms omit it |
| `OAC_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
| `OAC_LOG_FORMAT` | `auto` | `auto`, `text` or `json` |
| `OAC_LOG_ADD_SOURCE` | unset | `1` adds source locations |
Expand Down Expand Up @@ -124,7 +123,7 @@ The installer creates the installation directory, `~/.oac/core` by default, with
| Path | Content | Changed by |
| --- | --- | --- |
| `.env` | [Process settings](#process-settings-configjson). The file you edit | You, then `oac apply` |
| `compose.yaml`, `ports.yaml` | The release's service definition. Do not edit them | The release |
| `compose.yaml` | The release's service definition. Do not edit them | The release |
| `oac` | The [management command](./getting-started/operations.md#the-oac-command), copied from the Core image | The installer |
| `data/secrets/web/core.key` | The [Core key](./getting-started/operations.md#core-key) | `oac rotate-core-key` |
| `data/secrets/core/credential.key` | Encryption key for what Core stores sealed in the database | Nothing. Keep it with the database |
Expand Down
Loading
Loading