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