openship Documentation

Self-hosted deployment platform

10,299 stars TypeScript TypeScript 1 file Β· ~3,102 tokens #agents#ai#deployments#self-hosted
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy openship on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name openship --restart always postgres:16-alpine
Configured Services & Ports
βš™οΈ postgres
image: postgres:16-alpine
βš™οΈ redis
image: redis:7-alpine
βš™οΈ api
image: build locally
πŸ”Œ Ports: ${OPENSHIP_BIND_ADDR:-127.0.0.1}:${API_PORT:-4000}:${API_PORT:-4000}
βš™οΈ dashboard
image: build locally
πŸ”Œ Ports: ${OPENSHIP_BIND_ADDR:-127.0.0.1}:${DASHBOARD_PORT:-3001}:${DASHBOARD_PORT:-3001}
βš™οΈ web
image: build locally
πŸ”Œ Ports: ${OPENSHIP_BIND_ADDR:-127.0.0.1}:${WEB_PORT:-3000}:${WEB_PORT:-3000}
docker-compose.yml
yaml (docker-compose.yml)
# ──────────────────────────────────────────────────────────
# Openship β€” Docker Compose (SaaS / from-source control plane)
#
#   cp .env.example .env          # then edit
#   docker compose up -d --build
#
# This is the CONTROL PLANE: it BUILDS api + dashboard + web from source and
# runs postgres + redis. It's what we deploy for the openship.io SaaS, and what
# a from-source contributor runs locally. The MODE is decided entirely by .env β€”
# no profiles, no per-mode service variants:
#   β€’ SaaS (our env):   CLOUD_MODE=true + OPENSHIP_TARGET=cloud-saas + Oblien /
#                       GitHub App / secrets. Deployed apps run in Oblien
#                       sandboxes β€” the control plane never runs them here, so
#                       there is deliberately NO Docker socket and NO edge.
#   β€’ From-source dev:  CLOUD_MODE=false (the .env.example default).
#
# SELF-HOSTING (run your own apps as host containers with the OpenResty edge on
# :80/:443) is a DIFFERENT stack β€” the pull-based images + `edge` + Docker-out-
# of-Docker. That lives in docker/docker-compose.yml (or install it with the
# `openship up` CLI). Don't add the edge/socket here; this stays a clean,
# unprivileged control plane.
#
# Services:
#   β€’ postgres β†’ private :5432   (storage)
#   β€’ redis    β†’ private :6379   (queue + cache + rate-limit)
#   β€’ api      β†’ :4000   (override: API_PORT)
#   ‒ dashboard→ :3001   (override: DASHBOARD_PORT)
#   β€’ web      β†’ :3000   (override: WEB_PORT)   landing site (openship.io)
#
# The API + dashboard bind fixed internal ports via PORT (the API honors PORT
# regardless of OPENSHIP_TARGET); a reverse proxy maps the public domains to
# them. postgres/redis are internal-only. The API auto-migrates Postgres on boot.
#
# REMOTE ACCESS: reaching this from another machine (LAN IP or a reverse proxy)
# requires OPENSHIP_PUBLIC_URL in .env β€” else the dashboard loads but login is
# rejected (403 ORIGIN_REJECTED). Behind a proxy also set TRUST_PROXY=true.
#
# BIND INTERFACE: the api/dashboard/web ports publish on 127.0.0.1 (loopback)
# by default, so nothing is exposed off-box unless you say so β€” a same-host
# reverse proxy still reaches them over loopback. If your proxy runs on ANOTHER
# machine (or you want direct off-box access), set OPENSHIP_BIND_ADDR to the
# interface to publish on, e.g. OPENSHIP_BIND_ADDR=0.0.0.0 (all) or a specific
# LAN IP. Prefer fronting with a proxy over binding 0.0.0.0.
# ──────────────────────────────────────────────────────────

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "20m"
    max-file: "3"

services:
  # ─── Database (storage, private) ──────────────────────
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    logging: *default-logging
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-openship}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-openship}
      POSTGRES_DB: ${POSTGRES_DB:-openship}
    # PRIVATE: not published to the host. api reaches it at postgres:5432.
    expose:
      - "5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-openship} -d ${POSTGRES_DB:-openship}"]
      interval: 5s
      timeout: 3s
      retries: 12

  # ─── Cache / Queue / Rate-limit (private) ─────────────
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    logging: *default-logging
    command: ["redis-server", "--appendonly", "yes"]
    # PRIVATE: not published to the host. api reaches it at redis:6379.
    expose:
      - "6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 12

  # ─── API (control plane) ──────────────────────────────
  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    restart: unless-stopped
    logging: *default-logging
    ports:
      # Loopback by default (nothing exposed off-box); OPENSHIP_BIND_ADDR opts
      # into a public/LAN interface. See the "BIND INTERFACE" note above.
      - "${OPENSHIP_BIND_ADDR:-127.0.0.1}:${API_PORT:-4000}:${API_PORT:-4000}"
    # Mode + secrets (CLOUD_MODE, DEPLOY_MODE, OPENSHIP_TARGET, Oblien, GitHub
    # App, auth secrets, …) all come from .env. The values below OVERRIDE it
    # with the fixed internal port + in-cluster service DNS (compose
    # `environment` wins over `env_file`).
    env_file:
      - .env
    environment:
      NODE_ENV: production
      PORT: "${API_PORT:-4000}"
      DATABASE_URL: postgresql://${POSTGRES_USER:-openship}:${POSTGRES_PASSWORD:-openship}@postgres:5432/${POSTGRES_DB:-openship}
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "bun -e \"fetch('http://127.0.0.1:${API_PORT:-4000}/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 40s

  # ─── Dashboard (app UI) ───────────────────────────────
  dashboard:
    build:
      context: .
      dockerfile: apps/dashboard/Dockerfile
    restart: unless-stopped
    logging: *default-logging
    ports:
      - "${OPENSHIP_BIND_ADDR:-127.0.0.1}:${DASHBOARD_PORT:-3001}:${DASHBOARD_PORT:-3001}"
    env_file:
      - .env
    environment:
      NODE_ENV: production
      PORT: "${DASHBOARD_PORT:-3001}"
      INTERNAL_API_URL: http://api:${API_PORT:-4000}
    depends_on:
      api:
        condition: service_healthy

  # ─── Web / Landing ────────────────────────────────────
  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    restart: unless-stopped
    logging: *default-logging
    ports:
      - "${OPENSHIP_BIND_ADDR:-127.0.0.1}:${WEB_PORT:-3000}:${WEB_PORT:-3000}"
    environment:
      NODE_ENV: production
      PORT: "${WEB_PORT:-3000}"

volumes:
  postgres_data:
  redis_data:

Openship


Quick Start

There's one decision to make first: how you run Openship itself (the control plane). Everything else is the same afterwards.

If you're… Run Openship as Where your apps run
Solo, one machine, no ops Desktop app A server you connect over SSH, or Openship Cloud
A team β€” or you want push-to-deploy / to host apps on your own box Self-hosted server (openship up) On that box (Compose mode) β€” or out to another server / Cloud (bare mode)
Not interested in running anything Openship Cloud Managed sandboxes, zero setup

[!TIP]
Solo? Use the desktop app. It runs Openship's control plane on your own machine only while the app is open β€” nothing is left running on an always-on server, nothing is exposed publicly. You only need an always-on server install once you want push-to-deploy (CI/CD), team access, or to host apps on that box β€” the things that need a public, always-on endpoint.

Solo β€” desktop app

The control plane runs locally and drives your servers over SSH. No login, no terminal, no public surface β€” download, open, done:

Platform Download
macOS (Apple Silicon) Openship-arm64.dmg
macOS (Intel) Openship-x64.dmg
Windows Openship-win32-x64.zip
Linux Openship.AppImage

Linux: chmod +x Openship.AppImage && ./Openship.AppImage. Already have the CLI? openship install fetches and launches it. Links always point at the newest release.

From the desktop app you connect a server (SSH) or Openship Cloud and deploy to it β€” the app itself doesn't host public apps on your laptop.

Team / always-on β€” self-hosted server

Install the CLI (it bundles the API + dashboard), then run openship β€” an interactive wizard creates the first admin, wires your domain, and installs Openship as a boot service. Run it again anytime to manage the instance.

bash
curl -fsSL https://get.openship.io | sh          # install  (or: npm i openship β€” needs Node 22+)
openship                                          # guided setup, then control panel

The install script brings its own Node when your system one is older than 22; a package-manager
install runs on the Node you already have.

For CI / headless boxes, skip the wizard and drive openship up directly:

bash
openship up                                       # install + start as a background service (boots + auto-restarts)
openship up --public-url https://openship.example.com   # + serve the dashboard on your domain (edge + TLS handled)

openship up picks how it runs for you:

  • On Linux with Docker β†’ Compose mode (the default). Brings up the full stack β€” Postgres, Redis, API, dashboard, and a containerized OpenResty edge on :80/:443 β€” from published images. This is the flavor that hosts your deployed apps on the same box, with automatic domains + Let's Encrypt TLS. Force it with --compose.
  • Everywhere else β†’ bare mode (macOS, Windows, or Linux without Docker). A single lightweight process with an embedded database β€” an always-on control plane that deploys apps out to a server (SSH) or Cloud, like the desktop app but always on and login-required. Force it with --bare.

A self-hosted instance always requires login (the admin you create in setup). openship open opens the dashboard Β· openship stop stops it Β· openship update upgrades Β· openship up --foreground runs attached.

Preview an unreleased build (dev). To run the CLI built straight from source β€” a branch, tag, or main ahead of the next release β€” install the from-source build:

bash
curl -fsSL https://get.openship.io/dev | sh                  # main (default)
curl -fsSL https://get.openship.io/dev | OPENSHIP_REF=dev sh  # a branch/tag (var goes on sh, not curl)
openship-dev                                     # same CLI, built from source
openship-dev update                              # pull latest source + rebuild (no release needed)

It installs as a separate openship-dev command with its own isolated home (~/.openship-dev) and boot service, so your production openship and its data are never touched. Needs Bun + git; it's an unverified dev build (the dashboard compile wants real RAM/CPU) β€” not a production path.

Deploy a project:

bash
cd your-project
openship init            # link this directory to a project
openship deploy

Full server guide + complete CLI reference: openship.io/docs.

Shell completion (bash/zsh/fish)

Two ways to enable Tab-completion for openship:

Setup Trade-off
Static file (recommended) openship completion <shell> > Instant shell startup. Regenerate after upgrading to pick up newly added commands.
Live-sourced add source <(openship completion <shell>) to your shell config Always reflects the currently installed version. Adds a small delay to every new shell session.

Static file:

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

Open a new terminal β€” done.

Live-sourced (zsh example):

bash
echo 'source <(openship completion zsh)' >> ~/.zshrc

Self-host with raw Docker Compose (no CLI)

The self-hosted stack lives in docker/docker-compose.yml and pulls published images from GitHub Container Registry (ghcr.io/oblien/*) β€” no build tooling, no monorepo compile. Run it from the repo root:

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

The stack is postgres + redis + api + dashboard + edge. The edge is OpenResty on :80/:443 as a container (network_mode: host) β€” routing + Let's Encrypt, no bare host install. Linux only (host networking); on mac/win use openship up (bare). The api container mounts the host Docker socket so the control plane can build + run your apps as host containers β€” it's host-privileged through the socket, so run it only on a trusted host.

Upgrade: pin OPENSHIP_VERSION in .env for reproducible pulls, then docker compose --env-file .env -f docker/docker-compose.yml pull && … up -d. openship update only reconciles a stack the CLI installed, and openship up would adopt this one β€” don't reach for either here. Build from source instead: add -f docker/docker-compose.build.yml … up -d --build.

Host operations (:80/:443 takeover, the mail engine, host terminal/port scans) need the container→host SSH channel, which openship up provisions and this path does not — the five manual steps are in .env.example under Host operations from the container, and the failure it produces is Troubleshooting → Host control channel. Everything else, including deploys, works without it.

The root docker-compose.yml is a different file: it's the SaaS / from-source control plane (builds from source, ships the marketing site, no edge/socket). It does not self-host your apps β€” use docker/docker-compose.yml above or openship up.


How It Works

Point Openship at a source β€” a GitHub repo, a local folder, or a prebuilt artifact β€” and it runs one pipeline end to end:

  1. Detect. It reads your package.json, framework config, lockfiles, and any docker-compose.yml / openship.json to work out the stack, package manager, build/start commands, and port. Zero config files required; an openship.json overrides the guesses if you want control.
  2. Build. On the target server or locally on the orchestrator, into a Docker image or a bare release. The resolved config is frozen into a snapshot, so redeploys and rollbacks re-run exactly what shipped.
  3. Run. As a container (published on loopback only β€” never a public port) or a supervised host process.
  4. Route + secure. The OpenResty edge writes a reverse-proxy vhost to your domain and issues a Let's Encrypt certificate (HTTP-01). Because routing and TLS happen after the app is up, a DNS or cert hiccup surfaces as "action required" β€” it never fails the deploy or takes your app down.
  5. Push-to-deploy. A GitHub webhook re-runs the pipeline on every push to the tracked branch β€” rebuilding only the services a monorepo push actually touched.

Databases, domains, SSL, CDN, mail, and backups are managed from the same place. (Push-to-deploy and public domains need an always-on server or Cloud β€” a desktop/loopback instance has no public endpoint to receive webhooks.)


Interfaces

Three ways to drive the same backend:

  • Desktop app β€” full GUI, real-time logs, one-click everything. Best for solo.
  • Web dashboard β€” the same UI in the browser, built for teams.
  • CLI β€” scriptable and CI-friendly; also how you install and manage a self-hosted instance.

An MCP endpoint (for AI agents) and a REST API round it out for automation. Only routes that opt in are exposed as MCP tools, every call re-checks your permissions, and credential/token routes can never become tools. Full reference at openship.io/docs.

[!NOTE]
The docs are actively being filled out. If something's missing or unclear, contributions are hugely welcome.


Features

Built-in CI/CD Push-to-deploy, preview environments, staging/prod flows, rollbacks
Any stack Node, Python, Go, Rust, PHP, Ruby, Java, .NET, Docker, monorepos
Full backend Postgres, MySQL, MongoDB, Redis, workers, WebSockets, storage
Domains & SSL Automatic Let's Encrypt, wildcards, unlimited domains, auto-renewal
CDN Edge caching, HTTP/3, Brotli compression, instant purge
Mail server Built-in SMTP with DKIM/SPF/DMARC β€” no Mailgun or SES needed
Backups Scheduled, databases + volumes, one-click restore, export anytime
Real-time monitoring Live build logs, container metrics, visitor geography and per-code response mix β€” ~1.4 Β΅s per request, zero DB writes per request
Scaling Auto-scaling on cloud, multi-node ready on self-hosted
Portability Standard Docker containers β€” move between providers freely
Docker Compose Deploy existing compose files as-is

Deploy Anywhere

  • Openship Cloud β€” managed, auto-scaling, zero setup
  • Any VPS β€” Hetzner, DigitalOcean, Linode, OVH, and the rest
  • Dedicated servers β€” bare metal, colo, homelab
  • Multi-server β€” spread workloads across machines

Same interface regardless of where you deploy.


Status

Production-ready core, actively developed. Self-hosting is free (no billing).

Coming next: multi-node clusters, load-balancing UI, private networking, advanced monitoring, and visual CI/CD pipelines.


Contributing

See CONTRIBUTING.md.


Security

Found a vulnerability? We welcome your report β€” please disclose it privately,
never in a public issue, PR, or discussion.

  • Report it here (preferred): Report a vulnerability β€” a private GitHub advisory, visible only to you and the maintainers.
  • Scope, what to include, and our response/disclosure process: SECURITY.md.

Good-faith security research is authorized under our
safe-harbor policy, and we're happy to credit valid
first reports.

License

Openship is open-source software, licensed under the Apache License 2.0.

You may use, run, modify, self-host, and distribute it β€” including in commercial
and closed-source products β€” under the terms of the Apache 2.0 license. See
LICENSE for the full text.