← Back to dashboard
openshiphostingfreshReader view (for NotebookLM)

Openship Integration Guide

What is Openship?

The real model

The self-hosted deploy tier — and the reason codeAmani maintains a fork rather than running it stock.

Two compose files share a shape and have opposite jobs: docker/docker-compose.yml is the self-host stack (pulls ghcr.io/oblien/*, includes the OpenResty edge on :80/:443, mounts /var/run/docker.sock) while the ROOT docker-compose.yml is the from-source control plane and cannot host your apps. That socket mount is effectively host root, so an Openship Compose box is fully trusted infrastructure — never co-tenanted. codeAmani runs codeAmani-Solutions/open-ship (upstream oblien/openship, v0.6.5, Bun + Turbo monorepo) with a compose override that moves the fleet off the CLI's :4000 onto dashboard 3246 / API 3247, a deploy-agent SSH container because the API cannot SSH the WSL host, and Hazina-injected secrets split across .env (compose) and .env.local (operator) so mailbox passwords never enter the API container. Two fork fixes are load-bearing: the deploy-agent image must carry docker-buildx-plugin or docker build --progress=plain exits 125, and host /etc/letsencrypt must be bind-mounted because certsExist() inspects the deploy target's filesystem, not the edge's. Against the managed guides — vercel stays the default, render owns always-on managed processes, R2 serves assets at zero egress — Openship is the deliberate pick when the box has to be yours: several cost-sensitive KES-denominated apps on one Hetzner VPS, with Postgres, Redis, and a mail engine on the same host.

Six things you reach for first

One control plane, three interfaces, and an MCP endpoint — the CLI is also how you install and operate the instance.

Text
 ██████╗ ██████╗ ███████╗███╗   ██╗███████╗██╗  ██╗██╗██████╗
██╔═══██╗██╔══██╗██╔════╝████╗  ██║██╔════╝██║  ██║██║██╔══██╗
██║   ██║██████╔╝█████╗  ██╔██╗ ██║███████╗███████║██║██████╔╝
██║   ██║██╔═══╝ ██╔══╝  ██║╚██╗██║╚════██║██╔══██║██║██╔═══╝
╚██████╔╝██║     ███████╗██║ ╚████║███████║██║  ██║██║██║
 ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═══╝╚══════╝╚═╝  ╚═╝╚═╝╚═╝

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/openship about e-commerce order fulfilment. This guide is oblien/openship — the deployment platform at https://openship.io. The npm package openship is the deployment CLI.

Where it sits next to the managed hosts

VercelNetlifyRenderCloudflareOpenship
ModelManaged serverlessManaged serverlessManaged containersEdge WorkersSelf-hosted containers
Who runs the machineVercelNetlifyRenderCloudflareYou
Long-running processes✗ (functions)✗ (functions)✓✗ (isolates)✓
Cost at restPer seat / usagePer seat / usagePer servicePer requestVPS rent only
EgressMeteredMeteredMeteredR2 = freeYour provider's
Data residencyTheir regionsTheir regionsTheir regionsTheir edgeWherever you rack it
TLS / routingAutomaticAutomaticAutomaticAutomaticOpenResty + 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.

Official Documentation


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.

Bash
# 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
PowerShell
# 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:

Bash
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:

Bash
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

HostModeWhat you get
Linux with DockerCompose (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 Dockerbare (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.

Bash
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.yml is 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

Bash
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:

Bash
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

GroupCommands
Run / update the instanceup, stop, update, install, open, status, doctor, reset-admin-password
Deployinit, deploy, deployment, logs
Resourcesproject, service, domain
Access & APIlogin, logout, context, token, api
Edge & monitoringedge
Self-host infraserver, 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_:

Bash
openship login                                 # interactive
openship login --token opsh_pat_xxxxxxxxxxxx   # non-interactive / CI

Multiple instances are named contexts, stored in ~/.openship/config.json:

Bash
openship context            # list
openship context use prod   # switch

Shell completion — the static file is the fast option (regenerate after upgrades):

Bash
openship completion bash > /etc/bash_completion.d/openship
openship completion zsh  > ~/.zsh/completions/_openship
openship completion fish > ~/.config/fish/completions/openship.fish

Architecture

  • 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

TermMeaning
ProjectOne app. Remembers where the code comes from, how to build it, and everything attached.
ServiceA piece of a multi-part project — website, database, cache — running side by side.
DeploymentOne attempt to build and publish. Successful releases get version numbers (v1, v2…).
EnvironmentA separate copy of the project: Production or Preview.
DomainThe address people type, e.g. app.example.com.
Runtime / targetWhere 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.

Bash
# 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:

Bash
claude mcp add --transport http openship https://openship.example.com/api/mcp \
  --header "Authorization: Bearer opsh_pat_…"
JSON
{
  "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:

originhttps://github.com/codeAmani-Solutions/open-ship.git
upstreamhttps://github.com/oblien/openship.git (merged in; currently v0.6.5)
ShapeBun 1.3 + Turbo monorepo — apps/{api,cli,dashboard,desktop,edge,email,web}, packages/{adapters,core,db,db-email,onboarding,ui}
LicenseApache-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:

SurfaceHost portURL
Dashboard3246http://127.0.0.1:3246
API3247http://127.0.0.1:3247/api/health
Marketing web3236http://127.0.0.1:3236
Deploy-agent SSH2222ssh -p 2222 -i ssh-keys/id_ed25519 root@127.0.0.1
Webmail20000http://127.0.0.1:20000
Public dashboardCloudflare tunnelhttps://open-ship.codeamani.com → :3246
Public APICloudflare tunnelhttps://open-ship-api.codeamani.com → :3247

Bring-up (from the repo root, inside WSL):

Bash
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:

Bash
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:

Bash
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:

FileOwnerConsumed by
.envCompose stackdocker compose env_file for api / dashboard
.env.localhazina inject open-shipscripts/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):

Bash
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-ship writes .env.local; compose reads .env. Never commit either, never merge mailbox passwords into the compose env_file, never print admin / tunnel / mailbox / SSH private-key values. --check prints names only — use it in anything an agent can see.
  • The Docker socket is the blast radius. Compose mode mounts /var/run/docker.sock into the api container 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 from AFRICAN_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 CheckoutRequestID exactly as MPESA_PATTERNS.md describes.
  • 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

IssueFix
openship open fails / API not respondingopenship up, wait, retry. openship doctor for a full diagnosis.
Build failedRead 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 targetOpenship passes --progress=plain; the legacy builder rejects it. Install docker-buildx-plugin.
SSL verify says "no certs" although the edge issued themcertsExist() reads the deploy target's filesystem — bind-mount the host /etc/letsencrypt into it.
Agent unreachable after recreating the deploy targetThe API's SSH pool is pinned to the dead connection. Reconcile the server record (fork: node scripts/fleet-reconcile.mjs).
Dashboard /api/health 404The dashboard has no health route — probe /login returning 200 instead.
openship update refuses to touch a raw-Compose stackupdate only reconciles a stack the CLI installed. Pin OPENSHIP_VERSION and run docker compose … pull && … up -d.
Public dashboard 502 behind a tunnelCheck the tunnel origins point at the real dashboard/API ports, not the CLI's :4000.
npm i -g openship fails on the Node versionThe CLI needs Node 22+. Use the get.openship.io install script instead — it bundles its own Node.