# ────────────────────────────────────────────────────────── # Openship — environment reference (self-hosted AND SaaS) # # cp .env.example .env && docker compose up -d --build # # The SAME file drives both modes — flip CLOUD_MODE + fill the SaaS section. # docker-compose overrides DATABASE_URL / REDIS_URL with in-cluster service # DNS, so the localhost values here are for running the apps WITHOUT Docker. # ────────────────────────────────────────────────────────── NODE_ENV=production # ─── Mode (the ONLY switch — one compose stack, env decides) ── # false = self-hosted (default, no billing) | true = SaaS (billing, metering, multi-tenant) CLOUD_MODE=false # docker (default, self-hosted) | cloud (SaaS) DEPLOY_MODE=docker # SaaS only: hard cap on projects per user (cloud org = one owning user). # Enforced at project create + ensure. Self-hosted ignores this. Default 2. CLOUD_MAX_PROJECTS_PER_USER=2 # Runtime URL row (packages/core/runtime-config.ts). Leave unset for self-hosted. # For the SaaS set: OPENSHIP_TARGET=cloud-saas (app.openship.io / api.openship.io) # OPENSHIP_TARGET=local # ─── Storage (Postgres) ─────────────────────────────────── # Set DATABASE_URL → Postgres driver. Leave empty → PGlite embedded (dev only; # NOT for a multi-tenant SaaS). Compose builds this from POSTGRES_* below. DATABASE_URL=postgresql://openship:openship@localhost:5432/openship POSTGRES_USER=openship POSTGRES_PASSWORD=openship POSTGRES_DB=openship # PGLITE_DATA_DIR=/var/lib/openship/data # ─── Redis (queue + cache + rate-limit) ─────────────────── REDIS_URL=redis://localhost:6379 # Force the Redis-backed job runner / cache / rate-limiter and DISABLE the # silent in-memory fallback. Defaults ON when CLOUD_MODE=true; set true to force # it for self-hosted multi-replica too. false = auto-probe (single-box dev). OPENSHIP_REQUIRE_REDIS=true # Per-subsystem overrides (rarely needed): in-process|bullmq / memory|redis # OPENSHIP_JOB_RUNNER=bullmq # OPENSHIP_CACHE_STORE=redis # OPENSHIP_RATE_LIMIT_STORE=redis # ─── Auth (Better Auth) ─────────────────────────────────── # CHANGE BOTH SECRETS below before exposing this instance. Generate each with: # node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" BETTER_AUTH_SECRET=change-me-in-production # Internal-auth token fronting trusted endpoints. REQUIRED for any non-desktop # deploy — the API REFUSES to boot without it. INTERNAL_TOKEN=change-me-32-byte-random-hex # SaaS cross-subdomain SSO (must start with "." and be a parent of the API host). # Not needed for a self-hosted single-origin install. # BETTER_AUTH_COOKIE_DOMAIN=.openship.io # ─── Remote access (LAN IP or reverse-proxy domain) ────── # By default only http://localhost:3001 is trusted. To reach Openship from # ANOTHER machine — a reverse proxy on a different host, or a browser elsewhere # on your LAN — set OPENSHIP_PUBLIC_URL to the EXACT origin the browser uses # (include a non-standard port). Without it the dashboard loads but LOGIN is # rejected with 403 ORIGIN_REJECTED. The browser only ever talks to the # dashboard (:3001); the API stays internal behind the same-origin /api/proxy. # OPENSHIP_PUBLIC_URL=http://192.168.1.50:3001 # OPENSHIP_PUBLIC_URL=https://openship.example.com # # Behind a reverse proxy, trust its forwarded client IP — otherwise every user # is keyed to the proxy's IP, sharing ONE rate-limit bucket, and can lock each # other out of login. The proxy must set X-Real-IP to the real client. # TRUST_PROXY=true # # Additional browser origins to trust for CORS + CSRF (comma-separated) — only # needed when more than one host reaches the app. Same scheme+host+port the # browser uses. (OPENSHIP_PUBLIC_URL is already trusted automatically.) # OPENSHIP_EXTRA_TRUSTED_ORIGINS=http://192.168.1.50:3001 # Which host interface docker compose publishes the ports on. Default = all # interfaces (0.0.0.0). Set to a specific LAN IP to publish only there. # OPENSHIP_BIND_ADDR=192.168.1.50 # Ports docker compose publishes (host + container move together). Defaults # shown — override to avoid conflicts with something already on the host. # API_PORT=4000 # DASHBOARD_PORT=3001 # Certificate authority / ACME (self-hosted managed edge) # Defaults are unchanged when these are omitted: Let's Encrypt production, # Certbot's default EC key, and automatic renewal. # OPENSHIP_ACME_EMAIL=ops@example.com # OPENSHIP_ACME_DIRECTORY_URL=https://acme.zerossl.com/v2/DV90 # EAB credentials must be set together. The HMAC key must be base64url encoded. # OPENSHIP_ACME_EAB_KID= # OPENSHIP_ACME_EAB_HMAC_KEY= # OPENSHIP_ACME_KEY_TYPE=ec256 # ec256 | ec384 | rsa2048 | rsa4096 # OPENSHIP_ACME_CA_BUNDLE=/etc/ssl/private/acme-root.pem # OPENSHIP_ACME_TOS_AGREED=true # See docs/acme.md for ZeroSSL, private-CA, and container mount examples. # WEB_PORT=3000 # landing site — root control-plane compose (docker-compose.yml) only # ─── Image source (self-hosted pull-based compose) ─────── # The self-hosted stack (docker/docker-compose.yml) PULLS these published images # — no local build. Registry: ghcr.io/oblien (GitHub Container Registry — where # the official images are published). OPENSHIP_IMAGE_REGISTRY overrides it if you # mirror the images elsewhere. Pin OPENSHIP_VERSION to a release (e.g. 0.2.3) for # reproducible upgrades; `latest` tracks the newest release. To build that stack # from source, add docker/docker-compose.build.yml. (The root docker-compose.yml # control plane always builds from source.) # OPENSHIP_IMAGE_REGISTRY=ghcr.io/oblien # OPENSHIP_VERSION=latest # ─── Host operations from the container (optional) ──────── # The edge (routing/TLS) runs as the `edge` container; app containers run via # the mounted docker socket. For the few HOST-OS ops a container can't do to its # host (freeing a foreign proxy off :80/443, host system config), the API reaches # the host over SSH via host.docker.internal (internal bridge, not the public IP). # `openship up` provisions the key + these vars automatically; set them by hand # only for a raw `docker compose` install that needs host ops. # OPENSHIP_HOST_SSH_HOST=host.docker.internal # OPENSHIP_HOST_SSH_USER=root # OPENSHIP_HOST_SSH_PORT=22 # OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key # ─── OAuth login (optional) ─────────────────────────────── # GITHUB_CLIENT_ID= # GITHUB_CLIENT_SECRET= # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= # ══════════════════════════════════════════════════════════ # SaaS-only (CLOUD_MODE=true) # ══════════════════════════════════════════════════════════ # In CLOUD_MODE the API MUST use GitHub App auth (cli/token modes are rejected # at boot). Register a GitHub App and fill these: # GITHUB_APP_ID= GITHUB_APP_SLUG=openship-io # GITHUB_PRIVATE_KEY_BASE64= # base64 of the App's .pem private key # GITHUB_WEBHOOK_SECRET= # Oblien (cloud runtime + billing). Required for cloud deploys / metering. # OBLIEN_CLIENT_ID= # OBLIEN_CLIENT_SECRET= # OBLIEN_WEBHOOK_SECRET= # HMAC for /api/billing/oblien-webhook (else 503) # Billing feature switches (SaaS-owned; self-hosted + local proxy to the cloud). # Both OFF by default → billing shows "coming soon" everywhere and every # Stripe-mutating endpoint returns 403. Flip on the SaaS to go live — no # dashboard or self-hosted release needed. Enable subscriptions first, top-ups # when ready (top-ups also require BILLING_ENABLED=true). BILLING_ENABLED=false BILLING_TOPUPS_ENABLED=false # # To actually TRANSACT once BILLING_ENABLED=true, Stripe must be configured # (the switch gates the feature; these gate real charges). Missing key → the # checkout endpoints throw "Stripe is not configured"; missing/placeholder # price id → 503 BILLING_NOT_CONFIGURED. # STRIPE_SECRET_KEY= # STRIPE_WEBHOOK_SECRET= # signs POST /api/billing/webhook/stripe # Subscriptions ALSO need a published (non-null) price in PLANS + these ids; # until pricing is published the plans page shows "coming soon" and checkout # 400s BILLING_PLAN_NOT_PURCHASABLE (see [pricing-coming-soon]). # STRIPE_PRICE_PRO_MONTHLY= # STRIPE_PRICE_PRO_ANNUAL= # STRIPE_PRICE_TEAM_MONTHLY= # STRIPE_PRICE_TEAM_ANNUAL= # Top-up packs need only the real price ids (no null product-gate): # STRIPE_PRICE_PACK_5K= # STRIPE_PRICE_PACK_25K= # STRIPE_PRICE_PACK_100K= # Transactional email (optional) # SMTP_HOST= # SMTP_PORT=587 # SMTP_USER= # SMTP_PASS= # SMTP_FROM=Openship # Cloud waitlist (SaaS only): where the dashboard's "notify me" deploy gate # forwards emails. Read server-side by /api/cloud-waitlist; unset = accept # silently (no forward). Point at your marketing/waitlist submit endpoint. # MARKETING_API_URL=https://marketing.example.com/api/waitlist SYSTEM_DEBUG_LOGS=false