diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c7e547ff --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# Keep the build context to the source tree: no VCS history, no host build +# output (the image does its own release build), no Node artifacts. +# tools/jhm + make_dep_file are host-built (gitignored) binaries; if they +# leak into the context, `make bootstrap` skips bootstrap.sh and the .jhm +# tree-root marker it creates, and jhm dies at "Unable to find .jhm". +.git +.claude +tools/jhm +tools/make_dep_file +**/node_modules +**/dist +**/__pycache__ +**/package-lock.json +*.log diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..c56d5184 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,83 @@ +# Build (and on demand, publish) the Orly runtime image (#530). +# +# Pull requests touching the docker files build the image and gate it with +# the MCP smoke pointed at the CONTAINERIZED server (the same smoke.mjs that +# gates clients/mcp against a host orlyi) -- no push. A version tag or a +# manual dispatch additionally pushes to ghcr.io/orlyatomics/orly. +name: docker + +on: + pull_request: + paths: + - Dockerfile + - .dockerignore + - docker/** + - .github/workflows/docker.yml + push: + tags: ['v*'] + workflow_dispatch: + +jobs: + image: + name: build image + in-container smoke + runs-on: ubuntu-24.04 + timeout-minutes: 90 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + + - name: Build image + run: docker build -t ghcr.io/orlyatomics/orly:ci . + + - name: Start container + run: | + docker run -d --name orly -p 8082:8082 ghcr.io/orlyatomics/orly:ci + for _ in $(seq 1 60); do + if ss -tln | grep -q ':8082'; then exit 0; fi + sleep 1 + done + echo "container never opened :8082; logs:" + docker logs orly | tail -40 + exit 1 + + # The same MCP smoke that gates clients/mcp, but ORLY_URL points at the + # container and the `sample` package is the one BAKED into the image -- + # exercising install/new_pov/call/call_batch/error against the published + # artifact end to end. + - name: MCP smoke against the container + run: | + (cd clients/ts && npm install --silent && npx tsc) + (cd clients/mcp && npm install --silent && npx tsc) + ORLY_URL=ws://127.0.0.1:8082/ node clients/mcp/smoke/smoke.mjs + + # The image also claims in-container package compiles (orlyc + g++ + + # ORLY_SRC_ROOT). Gate that claim: compile a from-scratch user package + # to a loadable .so inside the running container. (This caught a + # missing uuid-dev in the runtime stage during development.) + - name: In-container orlyc compile + run: | + docker exec orly bash -c ' + set -e + mkdir -p /tmp/user + printf "package #1;\nhello = (42) where {};\n" > /tmp/user/hello.orly + cd /tmp/user && orlyc -o /tmp/user /tmp/user/hello.orly + test -s /tmp/user/hello.1.so + ' + + - name: Container logs (for diagnosis) + if: always() + run: docker logs orly 2>&1 | tail -60 + + - name: Push to ghcr.io + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + docker tag ghcr.io/orlyatomics/orly:ci ghcr.io/orlyatomics/orly:latest + docker push ghcr.io/orlyatomics/orly:latest + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + ver="${GITHUB_REF#refs/tags/}" + docker tag ghcr.io/orlyatomics/orly:ci "ghcr.io/orlyatomics/orly:${ver}" + docker push "ghcr.io/orlyatomics/orly:${ver}" + fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..80348cd4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# Orly runtime image (#530): a mem-sim `orlyi` with the example packages +# pre-compiled, plus `orlyc` and the source headers so user `.orly` packages +# compile INSIDE the container (orlyc shells out to g++ with -I$ORLY_SRC_ROOT). +# +# docker build -t ghcr.io/orlyatomics/orly . +# docker run --rm -p 8082:8082 ghcr.io/orlyatomics/orly +# +# The WebSocket + JSON protocol is then on ws://127.0.0.1:8082/ -- point any +# client driver (clients/{python,go,ts}) or the MCP server (clients/mcp) at it. + +# ---- build stage ----------------------------------------------------------- + +FROM ubuntu:24.04 AS build + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential gcc g++ uuid-dev libgmp-dev libaio-dev libsnappy-dev \ + libreadline-dev libboost-system-dev zlib1g-dev bison flex python3 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src/orly +COPY . . + +# Bootstrap jhm + nycr, then build just the two production binaries this +# image ships (jhm emits to /src/out_orly). `make version` degrades to +# "unknown" without git -- fine for an image whose tag carries the version. +ARG JHM_WORKER_COUNT=4 +RUN make bootstrap version \ + && PATH="/src/orly/tools:$PATH" \ + jhm -c release --worker-count "$JHM_WORKER_COUNT" \ + orly/server/orlyi orly/orlyc + +# Pre-compile the example packages with the just-built orlyc (each orlyc run +# stands up and tears down its embedded mem-sim server, so this also smokes +# the binaries during the image build). `sample` is the trivial write/read +# package the MCP smoke uses; `graph` is the agent knowledge-graph schema; +# `market` is the prediction market. +RUN mkdir -p /opt/orly-packages /tmp/pkgout && touch /opt/orly-packages/__orly__ \ + && cd /tmp/pkgout \ + && /src/out_orly/release/orly/orlyc -o /tmp/pkgout /src/orly/clients/mcp/smoke/sample.orly \ + && /src/out_orly/release/orly/orlyc -o /tmp/pkgout /src/orly/examples/agent-swarm/graph.orly \ + && /src/out_orly/release/orly/orlyc -o /tmp/pkgout /src/orly/examples/prediction-market/market.orly \ + && cp /tmp/pkgout/*.so /opt/orly-packages/ + +# ---- runtime stage --------------------------------------------------------- + +FROM ubuntu:24.04 + +# Runtime .so set verified via ldd on orlyi/orlyc, plus what in-container +# package compiles need: g++ (orlyc execs `g++ -I$ORLY_SRC_ROOT ...`) and +# uuid-dev (generated code includes base/uuid.h -> ; the lib +# alone is not enough -- verified by compiling a package in the container). +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + g++ uuid-dev libgmp10 libgmpxx4ldbl libaio1t64 libboost-system1.83.0 \ + libreadline8t64 libsnappy1v5 zlib1g \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /src/out_orly/release/orly/server/orlyi /usr/local/bin/orlyi +COPY --from=build /src/out_orly/release/orly/orlyc /usr/local/bin/orlyc +# The source tree = the header root orlyc-generated C++ compiles against. +COPY --from=build /src/orly /opt/orly/src +COPY --from=build /opt/orly-packages /var/lib/orly/packages +COPY docker/entrypoint.sh /usr/local/bin/orly-entrypoint + +# Trailing slash matters: the compiler concatenates paths onto this directly. +ENV ORLY_SRC_ROOT=/opt/orly/src/ + +EXPOSE 8082 +ENTRYPOINT ["/usr/local/bin/orly-entrypoint"] diff --git a/README.md b/README.md index 40beb096..93e7ca27 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,24 @@ speaking the same [WebSocket + JSON protocol](docs/PROTOCOL.md). ## Quick start -System dependencies (Ubuntu 24.04): +**Docker** (any host — the engine itself is Linux-only, so this is also the +macOS/Windows path): + +```sh +docker run --rm -p 8082:8082 ghcr.io/orlyatomics/orly +``` + +That's a solo mem-sim server with the `sample`, `graph`, and `market` example +packages pre-compiled and installable, speaking the +[WebSocket + JSON protocol](docs/PROTOCOL.md) on `ws://127.0.0.1:8082/` — +point any client driver ([`clients/`](clients/)) or the +[MCP server](clients/mcp/) at it. `orlyc` and the source headers are in the +image, so it compiles your own `.orly` packages too +(`docker exec orlyc -o /var/lib/orly/packages yourpkg.orly`). Extra +`docker run ... ` pass straight through to `orlyi` +([#530](https://github.com/orlyatomics/orly/issues/530)). + +**From source** — system dependencies (Ubuntu 24.04): ```sh sudo apt-get install -y \ diff --git a/changelog.d/530-docker-image.md b/changelog.d/530-docker-image.md new file mode 100644 index 00000000..19d94d39 --- /dev/null +++ b/changelog.d/530-docker-image.md @@ -0,0 +1 @@ +- **Added**: a publishable Docker runtime image (`Dockerfile` + `.github/workflows/docker.yml` → `ghcr.io/orlyatomics/orly`): a mem-sim `orlyi` with the `sample`/`graph`/`market` example packages pre-compiled, plus `orlyc`, `g++`, and the source headers (`ORLY_SRC_ROOT`) so user `.orly` packages compile inside the container. `docker run -p 8082:8082` is now the two-command onboarding path (pair with `clients/mcp` for agents), and the only way to run Orly on non-Linux hosts. PR builds gate the image with the MCP smoke pointed at the containerized server; version tags and manual dispatch publish (#530). diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..ef705d23 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Default container entrypoint (#530): a solo mem-sim orlyi with the baked +# example packages (sample, graph, market) installable out of the box, WS on +# 8082. Extra `docker run ... ` are appended verbatim, so any orlyi +# flag can be overridden or added. Sizing knobs via env for the common case. +set -e + +exec orlyi \ + --mem_sim \ + --mem_sim_mb="${ORLY_MEM_SIM_MB:-256}" \ + --mem_sim_slow_mb="${ORLY_MEM_SIM_SLOW_MB:-64}" \ + --create=true \ + --instance_name="${ORLY_INSTANCE_NAME:-orly}" \ + --starting_state=SOLO \ + --port_number=8080 \ + --slave_port_number=8081 \ + --ws_port_number=8082 \ + --reporting_port_number=8083 \ + --connection_backlog=32 \ + --package_dir=/var/lib/orly/packages \ + --max_parallel_frames 4000 \ + --page_cache_size 256 \ + --block_cache_size 64 \ + --le --log_info \ + "$@"