This page is the build plan, held to one bar: ease of self-host beats perfect infra, every time the two conflict. One container, embedded database, no DNS homework, auth that works without an IdP — and the exact same image running our hosted tier.
Self-host experience
The Gitea playbook, not the Kubernetes playbook. The default install has zero external dependencies: SQLite for metadata, local disk for blobs, both inside one volume. Postgres and S3 are config changes, not migrations of your mental model. This supersedes the compose-first story on the Technical page — compose is now Mode 1.
/dataDATABASE_URL + OBJECT_STORE_URL — that's the diffdock migrate --from-sqlite one-shot moverPer-artifact subdomains (full origin isolation) require wildcard DNS + a wildcard cert — the #1 thing that makes self-hosting "pages" products miserable. So sandboxing is tiered:
iframe sandbox="allow-scripts" without allow-same-origin — the browser gives the content an opaque origin, so scripts run but can touch no cookies, storage, or APIs. Safe on a bare IP address.ARTIFACT_HOST=*.artifacts.acme.com and artifacts get full-window, own-origin serving. Caddy bundled in the image does the wildcard TLS automatically via ACME DNS challenge.Rule: every feature degrades gracefully when its var is unset. Nothing crashes on missing config.
Auth
The killer failure mode for self-hosted tools is "now configure your IdP" on day one. Dock's ladder: local accounts → SMTP magic links → OIDC → SAML/SCIM, each unlocked by config, none required to start. Machine auth (CLI device flow, agent tokens) is identical in every mode and specced on the Spec page.
| Rung | How humans sign in | Needs |
|---|---|---|
| Local | Admin creates users in the wizard / invites via link. Email + password, argon2id, optional TOTP. | nothing |
| Magic link | Email link sign-in; invite flows get nicer. | SMTP_URL |
| OIDC | "Continue with Okta/Entra/Google" — JIT-provisioned, org-gated by email domain or IdP group. | 3 env vars |
| SAML + SCIM | Legacy IdPs; auto-deprovisioning. Enterprise tier. | config + license |
Hosted tier is the same ladder with rungs pre-pulled: magic link + Google/Microsoft OAuth on by default, org = verified email domain.
Parity
The trust contract with the OSS community: the hosted product runs the public image, and feature gates never live in the core. Differences exist only where physics or business demand them — and we publish the list.
| Capability | Self-host | Hosted |
|---|---|---|
| Publish, render, versions, comments, MCP, SSE, search | ✓ full engine | ✓ same image |
| Sign-in ladder | local → SMTP → OIDC → SAML | magic link + Google/Microsoft preconfigured |
| TLS + wildcard artifact domain | bundled Caddy/ACME, or your ingress | done for you (*.dock.build) |
your SMTP_URL | built-in | |
| Version retention | unlimited (your disk) | tier-based (comment-referenced versions always kept) |
| Billing module | dormant code path | active |
| Telemetry | off by default; opt-in, anonymous, source-visible | standard product analytics |
| Upgrades | docker pull; migrations run on boot | continuous |
| Support | community · paid contract (Enterprise) | included |
Single image ghcr.io/niftory/dock, semver tags, hosted always runs the latest public tag (we are the canary, not the special case). Migrations are forward-only and run on boot with a schema-version gate; N-1 image rollback is always safe because migrations within a minor version are additive-only. Backup story: Mode 0 = snapshot the volume; Mode 1+ = pg_dump + bucket versioning. Documented, tested in CI.
Codebase
MetaStore + BlobStore interfaces live in packages/core; sqlite/pg and fs/r2 drivers implement them and pass one shared test suite. The seam Unkey wished it had.vite.config.ts + server/ shape; Ghost's admin app). One process self-hosts everything; hosted serves the same build from Cloudflare.packages/cli); CLI and MCP share one protocol client.The stack, locked
Nothing speculative: each choice names the proven project we copied it from, with the receipt in their repo. The one anti-pattern we dodged is also named.
| Layer | Choice | Modeled on (receipt) |
|---|---|---|
| Monorepo | pnpm workspaces, apps/ + packages/ (Turbo when build graph grows) | n8n (pnpm-workspace.yaml, turbo.json) · Ghost (pnpm-workspace.yaml, nx.json) · Cal.com (turbo.json) |
| API runtime | One Node 22 container, Hono HTTP (web-standard, portable; zero framework lock-in) | Unkey post-mortem: built on CF Workers, rewrote to a stateful monolith — "self-hosting was nearly impossible" (InfoQ, Dec 2025) |
| Web app | TanStack Start (Vite) → static build; deployed to Cloudflare hosted, embedded in the container self-host | Outline (root vite.config.ts, app/+server/ one container) · Ghost (admin app served by core) |
| Database | SQLite default (better-sqlite3) → Neon Postgres via DATABASE_URL; same five-table schema | n8n (DB_TYPE defaults sqlite → postgresdb) · Ghost (sqlite dev / MySQL prod) |
| Blobs | Local disk default → S3-compatible / R2 via env (zero-egress = the hosting economics) | Outline (FILE_STORAGE=local|s3) |
| Render path | marked (GFM) + xss allowlist sanitizer; fflate for bundles; WebCrypto sha-256; nanoid ids | Pure-JS, runtime-agnostic picks — every dep runs on Node and edge alike |
| Tests / CI | Vitest; drivers verified against one shared suite; GitHub Actions | Standard across n8n / Cal.com / Outline |
| Self-host | docker run -v dock:/data ghcr.io/niftory/dock — wizard, env ladder, nothing required | n8n (docker run n8nio/n8n, SQLite default) · Gitea/PocketBase as the ergonomics bar |
| Hosted deploy | CF static (web) + a container or two (api, Fly/Hetzner/CF Containers) + Neon + R2; CF proxy caches immutable @vN. Not Vercel — egress economics. | Ghost(Pro)/n8n cloud (containers + managed DB) · Plausible (Hetzner) · Quick (one VM + object store) |
| License | Apache-2.0, no /ee split | Ghost (MIT) · Cal.com (now 100% MIT, no open-core split) |
Product mechanics (comments, share links, per-doc permissions) study Outline; self-host ergonomics study n8n; the business study is Ghost. Unkey is the tuition we didn't have to pay.
Build sequence
Maps the M1–M6 milestones to calendar weeks with hard exit criteria. Every phase ends with something dogfooded, not something demoed.
publish.@vN, restore, MD read-back with HTML→MD conversion, ?diff for MD. MCP server with publish/get.list_comments/reply_comment MCP, resolves[] on republish.migrate --from-sqlite, compose + Helm, docs site (published as Dock artifacts, obviously), conformance suite, backup/upgrade docs.Hosted tier hardening (billing, quotas, abuse pipeline, status page) runs weeks 10–14 in parallel — it's ops work on the same image, not product work.
Day-2 ops
Liveness, readiness (DB + blob probe), and /metrics in Prometheus format. Self-hosters' existing dashboards just work.
Per-org storage/rate quotas enforced in core (self-hosters want them too). Hosted adds an abuse-report endpoint and a one-flag takedown that 410s the artifact.
Self-host telemetry is off until the wizard asks once. Payload is a counted feature list, no content, no URLs — and the exact code that sends it is 50 lines you can read.
Go / no-go