Skip to content

Commit 2bdec7e

Browse files
committed
feat(docker): publish official runtime image ghcr.io/objectstack-ai/objectstack
Ship an official, versioned runtime image instead of only a copy-me example: - docker/Dockerfile: app-agnostic runtime — node:22-slim + pinned @objectstack/cli + `os start`, non-root `node` user, /api/v1/health HEALTHCHECK, OS_ARTIFACT_PATH/OS_PORT preset, OCI source labels. - .github/workflows/docker-publish.yml: reusable workflow (workflow_call + workflow_dispatch) building multi-arch (amd64/arm64) and pushing tags X.Y.Z / X.Y / X / latest, tag = published @objectstack/cli version, with a post-push `os --version` smoke test. Dispatch path covers base-image CVE rebuilds between releases. - release.yml: expose published/cli-version outputs from the changesets step and call docker-publish after every successful npm publish (a tags trigger would never fire — tags are pushed with GITHUB_TOKEN). - examples/docker: runtime stage now extends the official image; README documents tag pinning and the no-build mount path. - docs/deployment/self-hosting: Docker section leads with the official image (mount-and-run + extend), keeps a self-built equivalent for air-gapped registries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162o68e5w3bpUBEVRQboUGG
1 parent 28b7c28 commit 2bdec7e

7 files changed

Lines changed: 328 additions & 34 deletions

File tree

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 }}

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"]

docker/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# ObjectStack Official Runtime Image
2+
3+
`ghcr.io/objectstack-ai/objectstack` — the official production runtime for
4+
standalone ObjectStack apps. It packages Node 22 and `@objectstack/cli`
5+
(`os start`) and nothing else: **your compiled artifact is the app**, the
6+
image is the runtime.
7+
8+
```
9+
objectstack.config.ts ──(os build, CI)──▶ dist/objectstack.json ──(this image)──▶ running app
10+
```
11+
12+
## Tags
13+
14+
Published by [`docker-publish.yml`](../.github/workflows/docker-publish.yml)
15+
on every framework release. The image tag always equals the
16+
`@objectstack/cli` version inside the image:
17+
18+
| Tag | Meaning |
19+
|:---|:---|
20+
| `X.Y.Z` | Exact release — **pin this in production** |
21+
| `X.Y`, `X` | Rolling minor / major |
22+
| `latest` | Latest release — quick starts only |
23+
24+
Multi-arch: `linux/amd64` + `linux/arm64`.
25+
26+
## Usage
27+
28+
**Extend it** (the usual path — see [`examples/docker`](../examples/docker)):
29+
30+
```dockerfile
31+
FROM ghcr.io/objectstack-ai/objectstack:14.8.0
32+
COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json
33+
```
34+
35+
**Or run it directly** with a mounted or remote artifact — no image build:
36+
37+
```bash
38+
docker run -p 8080:8080 \
39+
-v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \
40+
-e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
41+
-e OS_AUTH_SECRET -e OS_SECRET_KEY \
42+
ghcr.io/objectstack-ai/objectstack:14.8.0
43+
```
44+
45+
`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come
46+
straight from your release storage.
47+
48+
## What the image presets
49+
50+
- `OS_ARTIFACT_PATH=/srv/app/objectstack.json`, `OS_PORT=8080`,
51+
`NODE_ENV=production`
52+
- Runs as the non-root `node` user
53+
- `HEALTHCHECK` on `/api/v1/health` (liveness); use `/api/v1/ready` as the
54+
readiness probe in orchestrators
55+
56+
**You must inject at runtime:** `OS_DATABASE_URL`, `OS_AUTH_SECRET`,
57+
`OS_SECRET_KEY` — never bake them into an image. Full variable catalog and
58+
reverse-proxy / multi-node guidance:
59+
[Self-Hosted Deployment](https://docs.objectstack.ai/docs/deployment/self-hosting).
60+
61+
## Local build of this image
62+
63+
```bash
64+
docker build -t objectstack:dev --build-arg OS_CLI_VERSION=14.8.0 docker/
65+
```

examples/docker/Dockerfile

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,14 @@ RUN npm ci
1919
COPY . .
2020
RUN npx os build # → dist/objectstack.json
2121

22-
# ── Runtime stage: CLI + artifact only ───────────────────────────────
23-
FROM node:22-slim
24-
WORKDIR /srv/app
25-
RUN npm install -g @objectstack/cli
26-
COPY --from=build /app/dist/objectstack.json ./objectstack.json
27-
28-
ENV NODE_ENV=production \
29-
OS_ARTIFACT_PATH=/srv/app/objectstack.json \
30-
OS_PORT=8080
31-
EXPOSE 8080
32-
33-
# Liveness: /api/v1/health. For orchestrated readiness use /api/v1/ready.
34-
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \
35-
CMD node -e "fetch('http://localhost:8080/api/v1/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
22+
# ── Runtime: the official ObjectStack runtime image ──────────────────
23+
# Ships Node + @objectstack/cli with `os start`, a non-root user, the
24+
# /api/v1/health HEALTHCHECK, and OS_ARTIFACT_PATH/OS_PORT preset (port 8080)
25+
# — see docker/README.md in the framework repo. Pin the tag to the
26+
# @objectstack/cli version in your package.json so the runtime matches the
27+
# CLI that built the artifact.
28+
FROM ghcr.io/objectstack-ai/objectstack:latest
29+
COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json
3630

3731
# OS_DATABASE_URL, OS_AUTH_SECRET, and OS_SECRET_KEY are injected at runtime —
3832
# never bake them into the image.
39-
CMD ["os", "start"]

0 commit comments

Comments
 (0)