Skip to content

Commit b50c0ef

Browse files
docs(deployment): lead self-hosting with Docker, demote bare Node (#8911) (#8960)
Docker is not one option among three — it is how the platform ships: docker/Dockerfile and .github/workflows/docker-publish.yml build and publish ghcr.io/objectstack-ai/objectstack on every framework release. The page's ordering said the opposite, and ordering is the strongest signal a docs page has. Re-framing, not a rewrite: the "Option 1/2/3" peer numbering is gone, the Docker section leads, Compose and Kubernetes read as shapes of that same path, and bare Node.js (systemd) moves after them as the minority path with a sentence on when it is the right choice. Its body is byte-identical to before the move. Image facts re-verified against the tree rather than copied from the prose: the registry/name and the tag ladder are as documented, but the example version was pinned at 14.8.0 while packages/cli is 17.0.0 (GA'd 2026-08-14) — a stale tag in what is now the lead position. Also made the "self-built runtime, equivalent" Dockerfile actually equivalent to docker/Dockerfile: non-root user and the OS_PORT-aware health check it was missing. Claude-Session: https://claude.ai/code/session_011RB4waLuNbdruCo6X9oobm Co-authored-by: Claude <noreply@anthropic.com>
1 parent f049f7f commit b50c0ef

1 file changed

Lines changed: 72 additions & 59 deletions

File tree

content/docs/deployment/self-hosting.mdx

Lines changed: 72 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
---
22
title: Self-Hosted Deployment
3-
description: Run a compiled ObjectStack app on your own infrastructure — bare Node.js, systemd, Docker, and Docker Compose with Postgres, including health checks, reverse-proxy wiring, and the secrets you must pin.
3+
description: Run a compiled ObjectStack app on your own infrastructure with the official Docker image — plus Compose with Postgres, Kubernetes, and the bare Node.js fallback, including health checks, reverse-proxy wiring, and the secrets you must pin.
44
---
55

66
# Self-Hosted Deployment
77

88
This guide takes the artifact produced by `os build` / `os compile` and runs it
9-
on infrastructure **you** operate: a Linux host, a Docker container, or a
10-
compose stack with Postgres. It assumes you have read
9+
on infrastructure **you** operate. **Docker is the standard path** — the
10+
platform publishes an official runtime image on every release, and Compose and
11+
Kubernetes are shapes of that same path rather than alternatives to it. Bare
12+
Node.js under systemd is the minority path, kept for hosts where a container
13+
runtime is not available or not permitted. It assumes you have read
1114
[Deployment Modes](/docs/deployment).
1215

1316
The deployment model is deliberately simple:
@@ -46,54 +49,17 @@ OS_SECRET_KEY=$(openssl rand -hex 32)
4649
The full catalog is in
4750
[Environment Variables](/docs/deployment/environment-variables).
4851

49-
## Option 1 — Bare Node.js (systemd)
50-
51-
The simplest deployment: Node 22+ and the CLI on a Linux host.
52-
53-
```bash
54-
# On the host — no repo clone, just the CLI and your artifact
55-
npm install -g @objectstack/cli
56-
scp dist/objectstack.json server:/opt/my-app/objectstack.json
57-
```
58-
59-
```ini title="/etc/systemd/system/my-app.service"
60-
[Unit]
61-
Description=My ObjectStack App
62-
After=network.target postgresql.service
63-
64-
[Service]
65-
Type=simple
66-
User=objectstack
67-
WorkingDirectory=/opt/my-app
68-
Environment=NODE_ENV=production
69-
Environment=OS_ARTIFACT_PATH=/opt/my-app/objectstack.json
70-
Environment=OS_PORT=8080
71-
EnvironmentFile=/opt/my-app/secrets.env # OS_DATABASE_URL, OS_AUTH_SECRET, OS_SECRET_KEY
72-
ExecStart=/usr/bin/os start
73-
Restart=on-failure
74-
75-
[Install]
76-
WantedBy=multi-user.target
77-
```
78-
79-
```bash
80-
sudo systemctl enable --now my-app
81-
curl -fsS http://localhost:8080/api/v1/health
82-
```
83-
84-
Upgrades are atomic: replace the artifact file and restart the service. Roll
85-
back by restoring the previous artifact.
86-
87-
## Option 2 — Docker (official image)
88-
89-
The artifact model maps cleanly onto containers, and ObjectStack ships an
90-
**official runtime image** for exactly this:
91-
`ghcr.io/objectstack-ai/objectstack` — Node 22 + `@objectstack/cli` +
92-
`os start`, running as a non-root user with a built-in health check and
93-
`OS_ARTIFACT_PATH` / `OS_PORT=8080` preset. Image tags mirror
94-
`@objectstack/cli` versions (`14.8.0`, `14.8`, `14`, `latest`) and the image
95-
is published multi-arch (amd64/arm64) on every framework release — **pin the
96-
exact version in production**, matching the CLI version in your
52+
## Docker (official image) — the standard path
53+
54+
The artifact model maps cleanly onto containers, and this is how the platform
55+
itself ships: ObjectStack builds and publishes an **official runtime image**
56+
on every framework release — `ghcr.io/objectstack-ai/objectstack`, Node 22 +
57+
`@objectstack/cli` + `os start`, running as a non-root user with a built-in
58+
health check and `OS_ARTIFACT_PATH` / `OS_PORT=8080` preset. Image tags mirror
59+
`@objectstack/cli` versions (`17.0.0`, `17.0`, `17`, `latest`) and the image is
60+
published multi-arch (amd64/arm64); the rolling `17.0` / `17` / `latest` tags
61+
move with every stable publish, while a prerelease gets only its exact tag.
62+
**Pin the exact version in production**, matching the CLI version in your
9763
`package.json`.
9864

9965
The fastest path needs no image build at all — hand the official image your
@@ -107,7 +73,7 @@ docker run -p 8080:8080 \
10773
-e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
10874
-e OS_AUTH_SECRET \
10975
-e OS_SECRET_KEY \
110-
ghcr.io/objectstack-ai/objectstack:14.8.0
76+
ghcr.io/objectstack-ai/objectstack:17.0.0
11177
```
11278

11379
(`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come
@@ -125,7 +91,7 @@ docker run -p 8080:8080 \
12591
-e OS_ARTIFACT_URL="https://releases.example.com/hotcrm-2.2.2.json#sha256=<64 hex chars>" \
12692
-e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \
12793
-e OS_AUTH_SECRET -e OS_SECRET_KEY \
128-
ghcr.io/objectstack-ai/objectstack:14.8.0
94+
ghcr.io/objectstack-ai/objectstack:17.0.0
12995
```
13096

13197
Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly
@@ -176,7 +142,7 @@ COPY . .
176142
RUN npx os build # → dist/objectstack.json
177143

178144
# ── Runtime: the official ObjectStack runtime image ──────────────────
179-
FROM ghcr.io/objectstack-ai/objectstack:14.8.0
145+
FROM ghcr.io/objectstack-ai/objectstack:17.0.0
180146
COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json
181147
```
182148

@@ -194,17 +160,20 @@ image)? The official image is nothing more than:
194160

195161
```dockerfile title="Dockerfile (self-built runtime, equivalent)"
196162
FROM node:22-slim
163+
RUN npm install -g @objectstack/cli@17.0.0
164+
197165
WORKDIR /srv/app
198-
RUN npm install -g @objectstack/cli@14.8.0
199-
COPY dist/objectstack.json ./objectstack.json
166+
RUN chown node:node /srv/app
167+
USER node
168+
COPY --chown=node:node dist/objectstack.json ./objectstack.json
200169

201170
ENV NODE_ENV=production \
202171
OS_ARTIFACT_PATH=/srv/app/objectstack.json \
203172
OS_PORT=8080
204173
EXPOSE 8080
205174

206175
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \
207-
CMD node -e "fetch('http://localhost:8080/api/v1/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
176+
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))"
208177

209178
CMD ["os", "start"]
210179
```
@@ -216,9 +185,11 @@ auto-minted dev crypto key inside a container — it lives on the ephemeral
216185
filesystem and dies with it.
217186
</Callout>
218187

219-
## Option 3 — Docker Compose with Postgres
188+
## Docker Compose with Postgres
220189

221-
A complete single-host production stack:
190+
The same path with a database attached — `build: .` is the Dockerfile from the
191+
section above, so the app container is still the official runtime image with
192+
your artifact on top. A complete single-host production stack:
222193

223194
```yaml title="docker-compose.yml"
224195
services:
@@ -354,6 +325,48 @@ afterwards. Restart only when the process is genuinely stuck, and note that a
354325
replica which *booted* without its database never re-runs its schema sync.
355326
</Callout>
356327

328+
## Bare Node.js (systemd) — without containers
329+
330+
The minority path, and a deliberate one: reach for it when the host has no
331+
container runtime available or permitted, when an existing systemd /
332+
configuration-management estate already owns process supervision, or when you
333+
are debugging directly on the box. It needs Node 22+ and the CLI on a Linux
334+
host, and nothing else.
335+
336+
```bash
337+
# On the host — no repo clone, just the CLI and your artifact
338+
npm install -g @objectstack/cli
339+
scp dist/objectstack.json server:/opt/my-app/objectstack.json
340+
```
341+
342+
```ini title="/etc/systemd/system/my-app.service"
343+
[Unit]
344+
Description=My ObjectStack App
345+
After=network.target postgresql.service
346+
347+
[Service]
348+
Type=simple
349+
User=objectstack
350+
WorkingDirectory=/opt/my-app
351+
Environment=NODE_ENV=production
352+
Environment=OS_ARTIFACT_PATH=/opt/my-app/objectstack.json
353+
Environment=OS_PORT=8080
354+
EnvironmentFile=/opt/my-app/secrets.env # OS_DATABASE_URL, OS_AUTH_SECRET, OS_SECRET_KEY
355+
ExecStart=/usr/bin/os start
356+
Restart=on-failure
357+
358+
[Install]
359+
WantedBy=multi-user.target
360+
```
361+
362+
```bash
363+
sudo systemctl enable --now my-app
364+
curl -fsS http://localhost:8080/api/v1/health
365+
```
366+
367+
Upgrades are atomic: replace the artifact file and restart the service. Roll
368+
back by restoring the previous artifact.
369+
357370
## Reverse proxy & TLS
358371

359372
Terminate TLS in front of the app (Caddy, nginx, Traefik, or your cloud LB)

0 commit comments

Comments
 (0)