Openship Integration Guide
Technology: openship · Category: hosting · Last reviewed: 2026-08-23
Source: https://tech-stack.codeamanilabs.org/guide/openship
Insight:
Openship is the self-hosted deploy tier: Apache-2.0 CI/CD that points at a repo and builds, ships, routes, and TLS-terminates it on hardware you own — a Vercel-shaped workflow without the per-seat bill or the lock-in. The trade you accept is that you are now the platform team: the OpenResty edge, Let's Encrypt renewals, Postgres, and backups are yours to keep alive. codeAmani runs a fork (
codeAmani-Solutions/open-ship) as its local fleet control plane, with secrets injected by Hazina and agents wired in over MCP.
██████╗ ██████╗ ███████╗███╗ ██╗███████╗██╗ ██╗██╗██████╗
██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██║██║██╔══██╗
██║ ██║██████╔╝█████╗ ██╔██╗ ██║███████╗███████║██║██████╔╝
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══██║██║██╔═══╝
╚██████╔╝██║ ███████╗██║ ╚████║███████║██║ ██║██║██║
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝╚═╝
Openship Integration Guide
Focus: Running codeAmani's own deploy platform — install the control plane, deploy a repo end to end, and operate the local fork (
codeAmani-Solutions/open-ship) that fronts the fleet.
Overview
Openship is an open-source, self-hostable deployment platform with built-in CI/CD, licensed Apache-2.0. Point it at a GitHub repo, a folder on disk, or a prebuilt artifact and it runs one pipeline end to end: detect the stack, build an image, run it on loopback, then write an OpenResty reverse-proxy vhost and issue a Let's Encrypt certificate for your domain. Push-to-deploy, preview environments, rollbacks, managed Postgres/MySQL/MongoDB/Redis, a built-in SMTP engine, and backups all live in the same control plane.
Three interfaces drive the same backend: a desktop app (Electron), a web dashboard (Next.js), and a CLI (npm i -g openship) — plus a REST API and an MCP endpoint so agents can drive it.
⚠️ Name collision. There is an unrelated project
openshiporg/openshipabout e-commerce order fulfilment. This guide isoblien/openship— the deployment platform at https://openship.io. The npm packageopenshipis the deployment CLI.
Where it sits next to the managed hosts
| Vercel | Netlify | Render | Cloudflare | Openship | |
|---|---|---|---|---|---|
| Model | Managed serverless | Managed serverless | Managed containers | Edge Workers | Self-hosted containers |
| Who runs the machine | Vercel | Netlify | Render | Cloudflare | You |
| Long-running processes | ✗ (functions) | ✗ (functions) | ✓ | ✗ (isolates) | ✓ |
| Cost at rest | Per seat / usage | Per seat / usage | Per service | Per request | VPS rent only |
| Egress | Metered | Metered | Metered | R2 = free | Your provider's |
| Data residency | Their regions | Their regions | Their regions | Their edge | Wherever you rack it |
| TLS / routing | Automatic | Automatic | Automatic | Automatic | OpenResty + certbot, you own the renewal |
Reach for Openship when the project must keep infra in-house, when a single €5–€20 VPS has to carry several apps that would each be a paid Render service, or when the same box must also host Postgres, Redis, and a mail engine. Stay on Vercel (codeAmani's primary host) for the marketing site, the dashboard, and anything where a git push to master shipping itself is worth more than owning the box.
flowchart LR
A["Source<br/>GitHub repo · local folder · artifact"] --> B["Detect<br/>package.json · lockfile · openship.json"]
B --> C["Build<br/>Docker image or bare release<br/>config frozen into a snapshot"]
C --> D["Run<br/>container on loopback only<br/>never a public port"]
D --> E["Route + secure<br/>OpenResty vhost + Let's Encrypt HTTP-01"]
E --> F["Live domain<br/>https://app.example.com"]
G["git push"] -.->|"webhook"| B
H["CLI · dashboard · desktop · MCP"] --> I["Control plane API<br/>Hono + Postgres/PGlite"]
I --> B
Official Documentation
| Resource | URL |
|---|---|
| Docs home | https://openship.io/docs |
| Quickstart | https://openship.io/docs/getting-started/quickstart |
| Installation | https://openship.io/docs/getting-started/installation |
| Core concepts | https://openship.io/docs/getting-started/core-concepts |
| First deployment | https://openship.io/docs/getting-started/first-deployment |
| Architecture overview | https://openship.io/docs/architecture/overview |
| Deploy from GitHub | https://openship.io/docs/guides/deploy-from-github |
| CLI reference | https://openship.io/docs/cli |
| API reference | https://openship.io/docs/api |
| MCP endpoint | https://openship.io/docs/mcp |
| Upstream repo | https://github.com/oblien/openship |
| Pricing / Cloud | https://openship.io/pricing |
Install
The install script brings its own Node when the system one is older than 22; the package-manager install runs on the Node you already have.
# macOS / Linux — fastest path (bundles Node)
curl -fsSL https://get.openship.io | sh
# or via a package manager (needs Node 22+)
npm i -g openship
# Windows (docs' one-liner — verify it resolves before trusting it; see NOTES.md)
irm https://git.openship.io/windows | iex
Then run the wizard — it creates the first admin, wires your domain, and installs Openship as a boot service:
openship # guided setup, then the control panel
openship open # opens the dashboard (default http://localhost:3001)
openship status # health
openship doctor # diagnose a broken install
For CI and headless boxes, skip the wizard:
openship up # install + start as a background service
openship up --public-url https://openship.example.com # + serve the dashboard on your domain (edge + TLS)
openship up --foreground # attached to the terminal
openship stop
openship update
Which mode openship up picks
| Host | Mode | What you get |
|---|---|---|
| Linux with Docker | Compose (default, force with --compose) |
Full stack from published ghcr.io/oblien/* images — Postgres, Redis, API, dashboard, and a containerized OpenResty edge on :80/:443. This is the flavour that hosts your deployed apps on the same box. |
| macOS / Windows / Linux without Docker | bare (force with --bare) |
One lightweight process with an embedded database. An always-on control plane that deploys out to a server over SSH or to Openship Cloud. |
Default ports: API :4000, dashboard :3001, edge :80/:443 in Compose mode.
Raw Docker Compose (no CLI)
The self-hosted stack lives in docker/docker-compose.yml and pulls published images — no monorepo compile.
git clone https://github.com/oblien/openship.git && cd openship
cp .env.example .env # then edit
docker compose --env-file .env -f docker/docker-compose.yml up -d
Linux only (the edge uses network_mode: host). The api container mounts the host Docker socket so the control plane can build and run your apps as host containers — that is host-privileged through the socket, so run it only on a trusted host.
The root
docker-compose.ymlis a different file — it is the from-source control plane (builds from source, ships the marketing site, no edge, no socket). It does not self-host your apps.
Deploy a project
cd your-project
openship init # link this directory to a project
openship deploy
openship deploy --watch # follow build logs
openship logs
Or from the dashboard: Library → Repositories, pick the repo, choose the target (Local / Your Server / Cloud), review the detected framework + build command + domain, press Deploy, and watch the logs stream.
Turn on CI:
openship project create --name my-app --git-owner codeAmani-Solutions --git-repo my-app
openship project git auto-deploy my-app --enable
A GitHub webhook then re-runs the pipeline on every push to the tracked branch — rebuilding only the services a monorepo push actually touched.
Push-to-deploy and public domains need an always-on server or Cloud. A desktop/loopback instance has no public endpoint for GitHub to call.
Detected stacks
Node, Python, Go, Rust, PHP, Ruby, Java, .NET, plain Docker images, Docker Compose files deployed as-is, and monorepos. Zero config files are required; an openship.json in the repo overrides the guesses when you want control.
CLI map
| Group | Commands |
|---|---|
| Run / update the instance | up, stop, update, install, open, status, doctor, reset-admin-password |
| Deploy | init, deploy, deployment, logs |
| Resources | project, service, domain |
| Access & API | login, logout, context, token, api |
| Edge & monitoring | edge |
| Self-host infra | server, system, mail, backup |
Global flags: --json (machine-readable — the one to use from scripts and agents), --version, --help.
Auth uses personal access tokens prefixed opsh_pat_:
openship login # interactive
openship login --token opsh_pat_xxxxxxxxxxxx # non-interactive / CI
Multiple instances are named contexts, stored in ~/.openship/config.json:
openship context # list
openship context use prod # switch
Shell completion — the static file is the fast option (regenerate after upgrades):
openship completion bash > /etc/bash_completion.d/openship
openship completion zsh > ~/.zsh/completions/_openship
openship completion fish > ~/.config/fish/completions/openship.fish
Architecture
flowchart TD
subgraph UI["Interfaces"]
D1["Dashboard · Next.js"]
D2["CLI · npm openship"]
D3["Desktop · Electron"]
D4["MCP · /api/mcp"]
end
UI -->|"HTTP /api/*"| API["Control plane API · Hono<br/>projects · deployments · domains<br/>env vars · backups · permissions"]
API --> DB[("Postgres + Drizzle<br/>or embedded PGlite")]
API -->|"getPlatform()"| ADP["@repo/adapters"]
ADP --> R["runtime<br/>Docker · bare process · cloud"]
ADP --> I2["infra<br/>OpenResty edge · certbot"]
ADP --> S["system<br/>docker/git prereq checks"]
R --> T{"Target"}
T --> T1["Local machine"]
T --> T2["Your server over SSH"]
T --> T3["Openship Cloud"]
- Control plane (API) — a Hono app that owns everything that matters and is the single source of truth. No other component writes to the database directly.
@repo/adapters— three subsystems:runtime(Docker containers, bare processes, cloud offload),infra(OpenResty routing + certbot TLS),system(prerequisite checks).- Database — Postgres with Drizzle ORM, or embedded PGlite for minimal-setup installs.
- Routing happens after the app is up. A DNS or certificate hiccup surfaces as "action required" — it never fails the deploy or takes a running app down.
Core concepts
| Term | Meaning |
|---|---|
| Project | One app. Remembers where the code comes from, how to build it, and everything attached. |
| Service | A piece of a multi-part project — website, database, cache — running side by side. |
| Deployment | One attempt to build and publish. Successful releases get version numbers (v1, v2…). |
| Environment | A separate copy of the project: Production or Preview. |
| Domain | The address people type, e.g. app.example.com. |
| Runtime / target | Where the app actually runs: Local, Your Server (SSH), or Openship Cloud. |
Driving Openship from Claude Code (MCP)
Openship exposes a single MCP endpoint at POST /api/mcp. Only routes that opt in become tools, every call re-runs the full auth and permission stack, and credential/token routes can never become tools.
# OAuth 2.1 — browser consent on first use (recommended)
claude mcp add --transport http --scope user openship https://openship.example.com/api/mcp
Static-token fallback:
claude mcp add --transport http openship https://openship.example.com/api/mcp \
--header "Authorization: Bearer opsh_pat_…"
{
"mcpServers": {
"openship": {
"url": "https://openship.example.com/api/mcp",
"headers": { "Authorization": "Bearer opsh_pat_…" }
}
}
}
Tools map onto REST routes — get_projects, get_deployments, post_deployments_build_access, get_domains, post_domains, get_github_repos, get_analytics, get_jobs, post_jobs_by_key_run. Call tools/list to see exactly what your token can reach; a read-only token yields only read tools.
Use the public HTTPS origin, not
localhost, so OAuth discovery and consent resolve in the browser.
codeAmani's fork — the local fleet control plane
codeAmani does not run stock Openship. The working checkout lives in WSL Ubuntu at
~/projects/grok-projects/open-ship:
origin |
https://github.com/codeAmani-Solutions/open-ship.git |
upstream |
https://github.com/oblien/openship.git (merged in; currently v0.6.5) |
| Shape | Bun 1.3 + Turbo monorepo — apps/{api,cli,dashboard,desktop,edge,email,web}, packages/{adapters,core,db,db-email,onboarding,ui} |
| License | Apache-2.0 (unchanged) |
Upstream's bun dev ports (:4000 / :3001) are not what this machine runs — 127.0.0.1:4000 is reserved for the globally installed openship CLI. The fleet topology is supplied by docker-compose.override.yml:
| Surface | Host port | URL |
|---|---|---|
| Dashboard | 3246 | http://127.0.0.1:3246 |
| API | 3247 | http://127.0.0.1:3247/api/health |
| Marketing web | 3236 | http://127.0.0.1:3236 |
| Deploy-agent SSH | 2222 | ssh -p 2222 -i ssh-keys/id_ed25519 root@127.0.0.1 |
| Webmail | 20000 | http://127.0.0.1:20000 |
| Public dashboard | Cloudflare tunnel | https://open-ship.codeamani.com → :3246 |
| Public API | Cloudflare tunnel | https://open-ship-api.codeamani.com → :3247 |
Bring-up (from the repo root, inside WSL):
hazina inject open-ship --check # names only — never values
hazina inject open-ship # writes .env.local
docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
node scripts/dev-status.mjs # host + merged product issues
What the fork adds
1. deploy-agent/ — an SSH target the control plane can actually reach.
When the API runs in Compose it cannot SSH to the WSL host (no sudo, no host sshd), so the fork ships an Ubuntu 24.04 container that is the deploy target: openssh-server with PasswordAuthentication no / PermitRootLogin prohibit-password, plus docker-ce-cli, docker-compose-plugin, and — critically — docker-buildx-plugin, because Openship runs docker build --progress=plain and the legacy builder rejects --progress with exit 125. It runs network_mode: host and mounts the host Docker socket, /var/lib/openship, and /etc/letsencrypt — that last bind is a fork fix: certsExist() inspects the deploy-agent's filesystem over SSH, so without it SSL verification cannot see certificates the edge already issued.
2. A product-issue operator loop. Containers up ≠ platform healthy — Openship has its own outage feed, so the fork wraps it in scripts:
node scripts/openship-api.mjs issues # session-cookie API client
node scripts/dev-status.mjs # host state + merged product issues
node scripts/dev-status.mjs --strict # non-zero exit if product outages exist
node scripts/fleet-reconcile.mjs # recycle the SSH pool + rebind the webmail container
node scripts/ops-watch.mjs --once # DONE / FAILED
fleet-reconcile.mjs exists because after a deploy-agent recreate the API's SSH pool stays pinned to the dead connection and GET /issues reports the agent unreachable even though ssh -p 2222 works. Reconcile PATCHes the server to invalidate the pool and rewrites deployment.container_id for the host-network webmail.
3. Agent MCP over subscription logins. The fork documents connecting Grok Build and Claude Code to the instance over Openship's OAuth — using grok login / claude login subscriptions, not XAI_API_KEY or ANTHROPIC_API_KEY:
claude mcp add --transport http --scope user openship \
https://open-ship.codeamani.com/api/mcp
grok mcp add --transport http openship \
https://open-ship.codeamani.com/api/mcp
OAuth resource/issuer is the public dashboard origin. A PAT (Authorization: Bearer ${OPENSHIP_MCP_TOKEN}) is the local fallback — and that is an Openship token, not an xAI or Anthropic key. Never hand-edit ~/.claude.json; use claude mcp add.
4. Webmail / apps/email work. Mailbox identity (display names, colours, BIMI-style brand avatars), a single CID-embedded HQ signature image, an Outlook-shaped settings shell (Mail + Accounts), a richer compose toolbar, PWA offline support, and OpenPGP passphrase encryption on send (apps/email/server/src/lib/encrypt-pgp.ts). The mail engine and its database are not in the compose file — Openship installed them as managed containers.
Secrets: Hazina, not .env by hand
Every secret for this checkout flows through the Hazina vault project open-ship. Two files, deliberately not collapsed:
| File | Owner | Consumed by |
|---|---|---|
.env |
Compose stack | docker compose env_file for api / dashboard |
.env.local |
hazina inject open-ship |
scripts/auto-login-local.mjs, operator tooling |
Do not env_file: .env.local into compose — mailbox passwords must never enter the API container. Bound names include OPENSHIP_ADMIN_EMAIL, OPENSHIP_ADMIN_PASSWORD, BETTER_AUTH_SECRET, INTERNAL_TOKEN, POSTGRES_PASSWORD, CLOUDFLARE_TUNNEL_TOKEN, PORKBUN_API_KEY / PORKBUN_SECRET_KEY, RESEND_API_KEY, and OPENSHIP_MCP_TOKEN. Values are revealed only on the operator machine (hazina get open-ship/… --reveal) and never printed in chat. See docs/HAZINA.md and docs/DEV-WORKFLOW.md in the fork.
Environment variables
Self-hosting keys read from .env (see .env.example in the repo):
OPENSHIP_VERSION= # pin a release for reproducible image pulls
OPENSHIP_PUBLIC_URL= # public origin the dashboard/API are served on
OPENSHIP_BIND_ADDR=127.0.0.1 # which interface published ports bind to
TRUST_PROXY= # set when a reverse proxy / tunnel fronts the stack
API_PORT=4000
DASHBOARD_PORT=3001
POSTGRES_PASSWORD=
BETTER_AUTH_SECRET=
INTERNAL_TOKEN=
GITHUB_CLIENT_ID= # optional: GitHub OAuth device flow on self-hosted
CLI-side, an opsh_pat_… token authenticates openship login --token and the MCP header.
codeAmani notes
- Secrets server-side only, always via Hazina.
hazina inject open-shipwrites.env.local; compose reads.env. Never commit either, never merge mailbox passwords into the composeenv_file, never print admin / tunnel / mailbox / SSH private-key values.--checkprints names only — use it in anything an agent can see. - The Docker socket is the blast radius. Compose mode mounts
/var/run/docker.sockinto theapicontainer so it can build and run apps as host containers. That is effectively root on the host. Run it only on a box you trust, keep the dashboard behind login (a self-hosted instance always requires the admin you create in setup), and expose it publicly only through the Cloudflare tunnel rather than opening ports. - Provenance still applies. Per
CLAUDE.md, the SLSA test is "does this project ship a downloadable artifact?" An Openship deploy is a deploy, not an artifact — no provenance target of its own. But anything codeAmani ships from an Openship-built pipeline (a CLI, an npm package, a release tarball) still needs Build L3 from the isolated builder in its own repo's workflow. Openship's build snapshot gives you reproducibility of the deploy; it is not attestation. - Cost is the whole argument for Kenya-targeted builds. The WhatsApp + M-Pesa apps in
codeAmani-labs-projects/are cost-sensitive and KES-denominated. Several of them on one Hetzner/DigitalOcean box under Openship — with Postgres, Redis, and the mail engine on the same host — beats a per-service managed bill that is priced in USD. Pick a region close to the users, and keep the low-bandwidth rules fromAFRICAN_MARKET_GUIDE.md(small bundles, lazy assets); Openship's CDN layer does Brotli and HTTP/3 but it cannot fix a 2 MB JS bundle. - M-Pesa callbacks need public HTTPS. Daraja will only call an HTTPS endpoint. A self-hosted Openship instance with a real domain and an auto-renewing Let's Encrypt certificate is a legitimate callback host — but it must be always-on (server or Cloud mode, not desktop/loopback), and you still verify the callback and deduplicate on
CheckoutRequestIDexactly asMPESA_PATTERNS.mddescribes. - Routing is not the primary host. Vercel stays codeAmani's default (
vercel/CLAUDE_CODE_INTEGRATION.md); Render remains the answer for a managed always-on process; Cloudflare R2 keeps serving assets at zero egress. Openship is the option you pick deliberately, when owning the box is the point.
Troubleshooting
| Issue | Fix |
|---|---|
openship open fails / API not responding |
openship up, wait, retry. openship doctor for a full diagnosis. |
| Build failed | Read the last red lines of the build log — usually a missing build command, an unset env var, or a wrong port. |
Host operations hang (:80/:443 takeover, mail engine, host terminal) |
The container→host SSH channel is missing. openship up provisions it; raw Compose does not — the five manual steps are in .env.example under Host operations from the container. See https://openship.io/docs/troubleshooting/host-channel |
docker build exits 125 on a custom deploy target |
Openship passes --progress=plain; the legacy builder rejects it. Install docker-buildx-plugin. |
| SSL verify says "no certs" although the edge issued them | certsExist() reads the deploy target's filesystem — bind-mount the host /etc/letsencrypt into it. |
| Agent unreachable after recreating the deploy target | The API's SSH pool is pinned to the dead connection. Reconcile the server record (fork: node scripts/fleet-reconcile.mjs). |
Dashboard /api/health 404 |
The dashboard has no health route — probe /login returning 200 instead. |
openship update refuses to touch a raw-Compose stack |
update only reconciles a stack the CLI installed. Pin OPENSHIP_VERSION and run docker compose … pull && … up -d. |
| Public dashboard 502 behind a tunnel | Check the tunnel origins point at the real dashboard/API ports, not the CLI's :4000. |
npm i -g openship fails on the Node version |
The CLI needs Node 22+. Use the get.openship.io install script instead — it bundles its own Node. |
Official docs: