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: 6 additions & 0 deletions .changeset/docker-hub-image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"perstack": patch
"@perstack/base": patch
---

feat: add official Docker Hub base image distribution
31 changes: 31 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
**/node_modules
**/.turbo
**/dist
.git/
.bun/

docs/
e2e/
benchmarks/
coverage/
clickhouse/
docker/out/

.claude/
.cursor/
.idea/
.vscode/
.next/

.env
.env.local
.env.*.local
.npmrc
*.log
*.tsbuildinfo
*.swp
*.swo
*.tmp
*.temp
.DS_Store
Thumbs.db
75 changes: 75 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Docker

on:
push:
tags:
- 'perstack@*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to build (e.g. perstack@0.0.93). Uses HEAD if empty.'
required: false

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
docker:
name: Build & Push
runs-on: ubuntu-24.04
steps:
- name: Extract version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
VERSION="${TAG#perstack@}"
MAJOR_MINOR="${VERSION%.*}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "major_minor=$MAJOR_MINOR" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Only tag as latest on push events (actual releases), not manual dispatches
if [ "${{ github.event_name }}" = "push" ]; then
echo "is_release=true" >> "$GITHUB_OUTPUT"
else
echo "is_release=false" >> "$GITHUB_OUTPUT"
fi

- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ steps.version.outputs.tag }}

- name: Setup QEMU
uses: docker/setup-qemu-action@v3

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
perstack/perstack:${{ steps.version.outputs.version }}
perstack/perstack:${{ steps.version.outputs.major_minor }}
${{ steps.version.outputs.is_release == 'true' && 'perstack/perstack:latest' || '' }}
perstack/perstack:sha-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
*.tsbuildinfo
coverage/
dist/
docker/out/
node_modules/
npm-debug.log*
perstack.lock
perstack.toml
!examples/**/perstack.toml
!benchmarks/**/perstack.toml
!apps/create-expert/perstack.toml
!docker/example/perstack.toml
perstack/
!apps/perstack
Thumbs.db
66 changes: 66 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Build stage: compile standalone binary with Bun
FROM --platform=$BUILDPLATFORM oven/bun:1 AS build

ARG TARGETARCH

WORKDIR /app

# Copy lockfile and workspace package.json files first for layer caching
COPY package.json bun.lock ./

# Apps
COPY apps/perstack/package.json apps/perstack/package.json
COPY apps/base/package.json apps/base/package.json
COPY apps/create-expert/package.json apps/create-expert/package.json
COPY apps/create-expert-skill/package.json apps/create-expert-skill/package.json

# Packages
COPY packages/core/package.json packages/core/package.json
COPY packages/runtime/package.json packages/runtime/package.json
COPY packages/tui/package.json packages/tui/package.json
COPY packages/tui-components/package.json packages/tui-components/package.json
COPY packages/installer/package.json packages/installer/package.json
COPY packages/skill-manager/package.json packages/skill-manager/package.json
COPY packages/perstack-toml/package.json packages/perstack-toml/package.json
COPY packages/log/package.json packages/log/package.json
COPY packages/filesystem/package.json packages/filesystem/package.json
COPY packages/react/package.json packages/react/package.json

# Providers
COPY packages/providers/core/package.json packages/providers/core/package.json
COPY packages/providers/anthropic/package.json packages/providers/anthropic/package.json
COPY packages/providers/openai/package.json packages/providers/openai/package.json
COPY packages/providers/google/package.json packages/providers/google/package.json
COPY packages/providers/azure-openai/package.json packages/providers/azure-openai/package.json
COPY packages/providers/bedrock/package.json packages/providers/bedrock/package.json
COPY packages/providers/vertex/package.json packages/providers/vertex/package.json
COPY packages/providers/ollama/package.json packages/providers/ollama/package.json
COPY packages/providers/deepseek/package.json packages/providers/deepseek/package.json

RUN bun install --frozen-lockfile

# Copy all source files
COPY . .

# Stub optional peer dependency that ink imports but doesn't need at runtime
RUN mkdir -p node_modules/react-devtools-core && \
echo "module.exports = { connectToDevTools() {} };" > node_modules/react-devtools-core/index.js

# Compile standalone binary (~100MB: 98MB Bun runtime + 2MB app code)
RUN TARGET="bun-linux-$(echo $TARGETARCH | sed 's/amd64/x64/' | sed 's/arm64/aarch64/')" && \
bun build apps/perstack/bin/cli.ts --compile --minify --target="$TARGET" --outfile=/app/out/perstack

# Runtime stage: agentic AI runtime needs a usable shell environment
# Agents invoke exec to run arbitrary commands, so include common tools
FROM ubuntu:24.04

RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates curl jq && \
rm -rf /var/lib/apt/lists/*

COPY --from=build /app/out/perstack /usr/local/bin/perstack

WORKDIR /workspace

ENTRYPOINT ["perstack"]
7 changes: 7 additions & 0 deletions docker/example/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM perstack/perstack:latest

COPY perstack.toml .

RUN perstack install

ENTRYPOINT ["perstack", "run", "my-expert"]
8 changes: 8 additions & 0 deletions docker/example/perstack.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[experts.my-expert]
model = "claude-sonnet-4-5"
version = "1.0.0"
description = "A helpful assistant."
instruction = "You are a helpful assistant. Answer questions concisely."

# @perstack/base runs in-memory by default (via InMemoryTransport).
# No additional configuration is needed for the base skill.
25 changes: 14 additions & 11 deletions docs/operating-experts/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,27 @@ Perstack runs on any Node.js environment. Choose your deployment target based on

## Docker

Build a container image for your Expert:
The official [`perstack/perstack`](https://hub.docker.com/r/perstack/perstack) base image ships pre-compiled standalone binaries — no Node.js or Bun needed at runtime.

```dockerfile
FROM node:22-slim
WORKDIR /app
COPY perstack.toml /app/perstack.toml
RUN npm install -g perstack
ENTRYPOINT ["perstack", "run", "--config", "/app/perstack.toml"]
FROM perstack/perstack:latest
COPY perstack.toml .
RUN perstack install
ENTRYPOINT ["perstack", "run", "my-expert"]
```

Run with isolation controls:

```bash
docker build -t my-expert .
docker run --rm \
-e ANTHROPIC_API_KEY \
-v $(pwd)/workspace:/workspace \
my-expert my-expert "query"
my-expert "query"
```

Pin to a specific version for reproducible builds:

```dockerfile
FROM perstack/perstack:0.0.93
```

For production, add resource limits and network restrictions. See [Isolation by Design](./isolation-by-design.md).
Expand Down Expand Up @@ -121,8 +124,8 @@ Deploy Experts from the registry without building custom images:
docker run --rm \
-e ANTHROPIC_API_KEY \
-v $(pwd)/workspace:/workspace \
node:22-slim \
npx perstack run @org/expert@1.0.0 "query"
perstack/perstack:latest \
run @org/expert@1.0.0 "query"
```

Pin versions for reproducible deployments.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"validate:changeset": "bun scripts/validate-changeset.ts",
"validate:versions": "bun scripts/validate-version-sync.ts",
"check:schema-diff": "bun scripts/check-schema-diff.ts",
"validate:all": "bun run validate:versions && bun run validate:changeset && bun run check:schema-diff"
"validate:all": "bun run validate:versions && bun run validate:changeset && bun run check:schema-diff",
"docker:build": "docker buildx build -f docker/Dockerfile -t perstack/perstack:local ."
},
"devDependencies": {
"@biomejs/biome": "^2.4.2",
Expand Down