🐳 openship

Self-hosted deployment platform

Docker Ready 5 Services 10,299 stars
1. Pull Image docker pull library/postgres
2. 1-Click Launch Command docker run -d --name openship postgres:16-alpine
3. Compose Deployment docker compose up -d
docker-compose.yml
Download .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.
# ──────────────────────────────────────────────────────────

services:
  # ─── Database (storage, private) ──────────────────────
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    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
    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
    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
    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
    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: