Skip to content

Commit df26c91

Browse files
authored
feat(docker): publish official runtime image ghcr.io/objectstack-ai/objectstack (#2952)
1 parent 8c693b3 commit df26c91

9 files changed

Lines changed: 366 additions & 35 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
feat(docker): official runtime image `ghcr.io/objectstack-ai/objectstack`
6+
7+
ObjectStack now ships an official, versioned Docker runtime image instead of
8+
only a copy-me example. The image packages Node 22 + a pinned
9+
`@objectstack/cli` + `os start` (non-root `node` user, `/api/v1/health`
10+
HEALTHCHECK, `OS_ARTIFACT_PATH` / `OS_PORT=8080` preset), published
11+
multi-arch (amd64/arm64) on every release with tags mirroring the CLI
12+
version (`X.Y.Z` / `X.Y` / `X` / `latest`).
13+
14+
Deploying an app is now:
15+
16+
```dockerfile
17+
FROM ghcr.io/objectstack-ai/objectstack:<version>
18+
COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json
19+
```
20+
21+
or, with no image build at all, `docker run` the official image with the
22+
artifact mounted (or `OS_ARTIFACT_PATH` pointing at an `https://` URL).
23+
`examples/docker` and the Self-Hosted Deployment docs now build on the
24+
official image; the self-built runtime Dockerfile remains documented for
25+
air-gapped registries.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Docker Publish
2+
3+
# Builds and pushes the official runtime image
4+
# ghcr.io/objectstack-ai/objectstack from docker/Dockerfile.
5+
#
6+
# Two entry points:
7+
# - workflow_call: invoked by release.yml right after a successful
8+
# `changeset publish`, with the just-published @objectstack/cli version.
9+
# (A plain `on: push: tags:` trigger would never fire — the release
10+
# workflow pushes its tags with GITHUB_TOKEN, and GitHub suppresses
11+
# workflow triggers from GITHUB_TOKEN-pushed refs.)
12+
# - workflow_dispatch: manual backfill / rebuild of an already-published
13+
# version, e.g. to pick up node:22-slim base-image CVE patches between
14+
# framework releases.
15+
#
16+
# The image tag always equals the @objectstack/cli version baked inside.
17+
18+
on:
19+
workflow_call:
20+
inputs:
21+
version:
22+
description: 'Published @objectstack/cli version to package (e.g. 14.8.0)'
23+
required: true
24+
type: string
25+
workflow_dispatch:
26+
inputs:
27+
version:
28+
description: '@objectstack/cli version to package (e.g. 14.8.0) — must already be on npm'
29+
required: true
30+
type: string
31+
32+
permissions:
33+
contents: read
34+
35+
jobs:
36+
publish:
37+
name: Build & push ghcr.io/objectstack-ai/objectstack
38+
runs-on: ubuntu-latest
39+
permissions:
40+
contents: read
41+
packages: write
42+
env:
43+
IMAGE: ghcr.io/objectstack-ai/objectstack
44+
steps:
45+
- name: Checkout repository
46+
uses: actions/checkout@v7
47+
48+
- name: Validate version input
49+
id: version
50+
env:
51+
VERSION: ${{ inputs.version }}
52+
run: |
53+
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$'; then
54+
echo "::error::'$VERSION' is not a valid semver version"
55+
exit 1
56+
fi
57+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
58+
59+
- name: Set up QEMU (arm64 emulation)
60+
uses: docker/setup-qemu-action@v3
61+
62+
- name: Set up Docker Buildx
63+
uses: docker/setup-buildx-action@v3
64+
65+
- name: Log in to ghcr.io
66+
uses: docker/login-action@v3
67+
with:
68+
registry: ghcr.io
69+
username: ${{ github.actor }}
70+
password: ${{ secrets.GITHUB_TOKEN }}
71+
72+
- name: Compute image tags
73+
id: meta
74+
uses: docker/metadata-action@v5
75+
with:
76+
images: ${{ env.IMAGE }}
77+
# Tag = CLI version. Rolling minor/major/latest tags move with every
78+
# publish; prereleases (x.y.z-rc.1) get only their exact tag.
79+
tags: |
80+
type=semver,pattern={{version}},value=${{ steps.version.outputs.version }}
81+
type=semver,pattern={{major}}.{{minor}},value=${{ steps.version.outputs.version }}
82+
type=semver,pattern={{major}},value=${{ steps.version.outputs.version }}
83+
type=raw,value=latest,enable=${{ !contains(steps.version.outputs.version, '-') }}
84+
85+
- name: Build and push
86+
uses: docker/build-push-action@v6
87+
with:
88+
context: docker
89+
platforms: linux/amd64,linux/arm64
90+
push: true
91+
build-args: |
92+
OS_CLI_VERSION=${{ steps.version.outputs.version }}
93+
tags: ${{ steps.meta.outputs.tags }}
94+
labels: ${{ steps.meta.outputs.labels }}
95+
96+
- name: Smoke-test the pushed image (amd64)
97+
# `os --version` proves the CLI resolved, installed, and runs on the
98+
# pushed image; a boot test needs an artifact + DB and belongs to the
99+
# examples/e2e suites, not here.
100+
env:
101+
VERSION: ${{ steps.version.outputs.version }}
102+
run: |
103+
docker pull "$IMAGE:$VERSION"
104+
docker run --rm "$IMAGE:$VERSION" os --version

.github/workflows/release.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ jobs:
1414
permissions:
1515
contents: write
1616
pull-requests: write
17+
outputs:
18+
published: ${{ steps.changesets.outputs.published }}
19+
cli-version: ${{ steps.cli-version.outputs.version }}
1720
steps:
1821
- name: Checkout repository
1922
uses: actions/checkout@v7
@@ -94,3 +97,34 @@ jobs:
9497
env:
9598
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9699
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
100+
101+
- name: Extract published @objectstack/cli version
102+
id: cli-version
103+
if: steps.changesets.outputs.published == 'true'
104+
# The fixed group bumps every public package in lockstep, so the CLI
105+
# is always in publishedPackages when a publish happened. Passed via
106+
# env (not inline interpolation) to avoid shell-quoting the JSON.
107+
env:
108+
PUBLISHED: ${{ steps.changesets.outputs.publishedPackages }}
109+
run: |
110+
version=$(jq -r '.[] | select(.name=="@objectstack/cli") | .version' <<<"$PUBLISHED")
111+
if [ -z "$version" ]; then
112+
echo "::error::publish succeeded but @objectstack/cli is missing from publishedPackages"
113+
exit 1
114+
fi
115+
echo "version=$version" >> "$GITHUB_OUTPUT"
116+
117+
docker:
118+
name: Docker image
119+
needs: release
120+
# Publish the official runtime image (ghcr.io/objectstack-ai/objectstack)
121+
# for every npm release. Called as a reusable workflow so the same build
122+
# can be re-run manually via workflow_dispatch (e.g. base-image CVE
123+
# rebuilds) — see docker-publish.yml.
124+
if: needs.release.outputs.published == 'true'
125+
permissions:
126+
contents: read
127+
packages: write
128+
uses: ./.github/workflows/docker-publish.yml
129+
with:
130+
version: ${{ needs.release.outputs.cli-version }}

.github/workflows/scaffold-e2e.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ on:
2020
paths:
2121
- 'packages/create-objectstack/**'
2222
- 'examples/docker/**'
23+
- 'docker/**'
2324
- '.github/workflows/scaffold-e2e.yml'
2425
schedule:
2526
- cron: '23 3 * * *'
@@ -114,13 +115,24 @@ jobs:
114115
curl -fsS http://localhost:8080/api/v1/ready
115116
kill "$SERVER_PID"
116117
118+
- name: Build official runtime image from this checkout
119+
# examples/docker builds FROM ghcr.io/objectstack-ai/objectstack.
120+
# Build that base HERE from docker/Dockerfile instead of pulling, so
121+
# (a) PRs exercise the official runtime Dockerfile itself, and
122+
# (b) the e2e stays hermetic — no dependency on a prior release
123+
# having published the tag (chicken-and-egg on the very first one).
124+
run: |
125+
docker build -t ghcr.io/objectstack-ai/objectstack:latest \
126+
--build-arg OS_CLI_VERSION=latest \
127+
"$GITHUB_WORKSPACE/docker"
128+
117129
- name: Docker build and run (examples/docker)
118130
run: |
119131
cd "$RUNNER_TEMP/e2e-app"
120132
cp "$GITHUB_WORKSPACE"/examples/docker/Dockerfile \
121133
"$GITHUB_WORKSPACE"/examples/docker/docker-compose.yml \
122134
"$GITHUB_WORKSPACE"/examples/docker/.dockerignore .
123-
docker build -t e2e-app .
135+
docker build --pull=false -t e2e-app .
124136
docker run -d --name e2e -p 18080:8080 \
125137
-e OS_SECRET_KEY="$(openssl rand -hex 32)" \
126138
-e OS_AUTH_SECRET="$(openssl rand -hex 32)" \

content/docs/deployment/self-hosting.mdx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,38 @@ curl -fsS http://localhost:8080/api/v1/health
8585
Upgrades are atomic: replace the artifact file and restart the service. Roll
8686
back by restoring the previous artifact.
8787

88-
## Option 2 — Docker
88+
## Option 2 — Docker (official image)
8989

90-
The artifact model maps cleanly onto containers: the image contains Node, the
91-
CLI, and one JSON file. The Dockerfile below (plus the compose stack in the
92-
next section and a `.dockerignore`) also ships ready-to-copy in
90+
The artifact model maps cleanly onto containers, and ObjectStack ships an
91+
**official runtime image** for exactly this:
92+
`ghcr.io/objectstack-ai/objectstack` — Node 22 + `@objectstack/cli` +
93+
`os start`, running as a non-root user with a built-in health check and
94+
`OS_ARTIFACT_PATH` / `OS_PORT=8080` preset. Image tags mirror
95+
`@objectstack/cli` versions (`14.8.0`, `14.8`, `14`, `latest`) and the image
96+
is published multi-arch (amd64/arm64) on every framework release — **pin the
97+
exact version in production**, matching the CLI version in your
98+
`package.json`.
99+
100+
The fastest path needs no image build at all — hand the official image your
101+
compiled artifact:
102+
103+
```bash
104+
os build # → dist/objectstack.json (or in CI)
105+
106+
docker run -p 8080:8080 \
107+
-v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \
108+
-e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
109+
-e OS_AUTH_SECRET \
110+
-e OS_SECRET_KEY \
111+
ghcr.io/objectstack-ai/objectstack:14.8.0
112+
```
113+
114+
(`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come
115+
straight from release storage instead of a mount.)
116+
117+
For a self-contained deployable image, extend it. The Dockerfile below (plus
118+
the compose stack in the next section and a `.dockerignore`) ships
119+
ready-to-copy in
93120
[`examples/docker`](https://github.com/objectstack-ai/framework/tree/main/examples/docker)
94121
— drop the files into your scaffolded project.
95122

@@ -102,11 +129,28 @@ RUN npm ci
102129
COPY . .
103130
RUN npx os build # → dist/objectstack.json
104131

105-
# ── Runtime stage: CLI + artifact only ───────────────────────────────
132+
# ── Runtime: the official ObjectStack runtime image ──────────────────
133+
FROM ghcr.io/objectstack-ai/objectstack:14.8.0
134+
COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json
135+
```
136+
137+
```bash
138+
docker build -t my-app .
139+
docker run -p 8080:8080 \
140+
-e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
141+
-e OS_AUTH_SECRET \
142+
-e OS_SECRET_KEY \
143+
my-app
144+
```
145+
146+
Prefer to build the runtime yourself (air-gapped registry, custom base
147+
image)? The official image is nothing more than:
148+
149+
```dockerfile title="Dockerfile (self-built runtime, equivalent)"
106150
FROM node:22-slim
107151
WORKDIR /srv/app
108-
RUN npm install -g @objectstack/cli
109-
COPY --from=build /app/dist/objectstack.json ./objectstack.json
152+
RUN npm install -g @objectstack/cli@14.8.0
153+
COPY dist/objectstack.json ./objectstack.json
110154

111155
ENV NODE_ENV=production \
112156
OS_ARTIFACT_PATH=/srv/app/objectstack.json \
@@ -119,15 +163,6 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \
119163
CMD ["os", "start"]
120164
```
121165

122-
```bash
123-
docker build -t my-app .
124-
docker run -p 8080:8080 \
125-
-e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
126-
-e OS_AUTH_SECRET \
127-
-e OS_SECRET_KEY \
128-
my-app
129-
```
130-
131166
<Callout type="warn">
132167
**Never bake `OS_AUTH_SECRET` / `OS_SECRET_KEY` into the image.** Pass them at
133168
runtime from your orchestrator's secret store. And never rely on the

docker/Dockerfile

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ObjectStack official runtime image — ghcr.io/objectstack-ai/objectstack
2+
#
3+
# A generic, app-agnostic production runtime: Node + @objectstack/cli +
4+
# `os start`. It contains NO app — bring your compiled artifact
5+
# (dist/objectstack.json, built by `os build` in CI):
6+
#
7+
# FROM ghcr.io/objectstack-ai/objectstack:<version>
8+
# COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json
9+
#
10+
# or run it without any image build at all:
11+
#
12+
# docker run -p 8080:8080 \
13+
# -v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \
14+
# -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
15+
# -e OS_AUTH_SECRET -e OS_SECRET_KEY \
16+
# ghcr.io/objectstack-ai/objectstack:<version>
17+
#
18+
# OS_ARTIFACT_PATH also accepts an https:// URL, so the artifact can be
19+
# fetched from your release storage instead of copied in.
20+
#
21+
# Published by .github/workflows/docker-publish.yml on every framework
22+
# release; the image tag always matches the @objectstack/cli version inside.
23+
# Docs: https://docs.objectstack.ai/docs/deployment/self-hosting
24+
25+
FROM node:22-slim
26+
27+
# Pinned by CI to the @objectstack/cli release that triggered the publish.
28+
# `latest` is only the fallback for ad-hoc local builds of this file.
29+
ARG OS_CLI_VERSION=latest
30+
RUN npm install -g @objectstack/cli@${OS_CLI_VERSION} \
31+
&& npm cache clean --force
32+
33+
LABEL org.opencontainers.image.source="https://github.com/objectstack-ai/framework" \
34+
org.opencontainers.image.title="ObjectStack" \
35+
org.opencontainers.image.description="Official ObjectStack runtime: os start + your compiled objectstack.json artifact" \
36+
org.opencontainers.image.licenses="Apache-2.0"
37+
38+
WORKDIR /srv/app
39+
RUN chown node:node /srv/app
40+
USER node
41+
42+
ENV NODE_ENV=production \
43+
OS_ARTIFACT_PATH=/srv/app/objectstack.json \
44+
OS_PORT=8080
45+
EXPOSE 8080
46+
47+
# Liveness: /api/v1/health. For orchestrated readiness use /api/v1/ready.
48+
# No backticks/$ in the -e script — this CMD runs under `/bin/sh -c`.
49+
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \
50+
CMD node -e "fetch('http://localhost:'+(process.env.OS_PORT||8080)+'/api/v1/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
51+
52+
# OS_DATABASE_URL, OS_AUTH_SECRET, and OS_SECRET_KEY are injected at runtime —
53+
# never bake them into an image.
54+
CMD ["os", "start"]

0 commit comments

Comments
 (0)