mirror of
https://github.com/oblien/openship.git
synced 2026-10-01 23:34:46 +08:00
274 lines
16 KiB
Bash
274 lines
16 KiB
Bash
# ──────────────────────────────────────────────────────────
|
|
# 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
|
|
# If the upstream edge already rate-limits API traffic, delegate the coarse
|
|
# pre-auth flood ceiling to it. Per-route user/auth limits remain enabled.
|
|
# Cloud mode implies this trust; standalone APIs keep the guard by default.
|
|
# OPENSHIP_TRUST_EDGE=false
|
|
#
|
|
# 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 Docker socket ───────────────────────────────────
|
|
# The API drives the edge and every deployed app container through the host's
|
|
# Docker daemon, mounted into the api container at /var/run/docker.sock (which is
|
|
# dockerode's own default, so only the HOST side of that mount is configurable).
|
|
# The host side MOVES: a rootless daemon runs as the invoking user and keeps its
|
|
# socket under that user's runtime dir, e.g. /run/user/1000/docker.sock.
|
|
#
|
|
# `openship up` resolves it from $DOCKER_HOST, the active docker context, or the
|
|
# rootless runtime dirs, and writes this key only when the answer isn't the default
|
|
# — set it by hand when that answer is wrong, or for a raw `docker compose` install.
|
|
# Getting it wrong does NOT fail loudly: Docker creates a missing bind-mount source
|
|
# as an empty DIRECTORY, so the stack comes up healthy and every container
|
|
# operation then fails in the transport, naming no path at all (#482). Yours:
|
|
# docker context inspect --format '{{.Endpoints.docker.Host}}'
|
|
# OPENSHIP_DOCKER_SOCKET=/var/run/docker.sock
|
|
|
|
# Abort a Docker image build after this many milliseconds with no output from
|
|
# the builder. This bounds genuinely stalled/OOM-thrashing builds without
|
|
# guessing from unrelated host containers. Default 10 minutes; accepted range
|
|
# is 1 minute to 24 hours.
|
|
# OPENSHIP_BUILD_IDLE_TIMEOUT_MS=600000
|
|
|
|
# ─── 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 mail engine,
|
|
# writing a catalog app's generated config file), the API reaches the host over
|
|
# SSH via host.docker.internal (internal bridge, not the public IP).
|
|
#
|
|
# Leaving this unset does NOT degrade to running those ops locally: inside a
|
|
# container "locally" is the container's own filesystem, so they REFUSE instead,
|
|
# naming this channel. Ordinary deploys are unaffected — they go through the
|
|
# docker socket. On a CLI install `openship doctor` reports which state you're
|
|
# in; on a raw `docker compose` install it can't see the stack, so the signal is
|
|
# the api's boot log (a `!!! HOST CONTROL …` banner, or silence when it's fine).
|
|
#
|
|
# `openship up` provisions all of it. By hand, on docker/docker-compose.yml, the
|
|
# vars are the LAST step, not the only one — all five are needed:
|
|
# 1. sudo mkdir -p /var/lib/openship/host-ssh
|
|
# sudo ssh-keygen -t ed25519 -N '' -C openship-host-executor \
|
|
# -f /var/lib/openship/host-ssh/id_ed25519
|
|
# 2. append the .pub to the authorized_keys of the user below — root, for the
|
|
# root-owned paths host ops touch — as ONE restricted line:
|
|
# printf 'from="172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,127.0.0.1",restrict,pty %s\n' \
|
|
# "$(sudo cat /var/lib/openship/host-ssh/id_ed25519.pub)" \
|
|
# | sudo tee -a /root/.ssh/authorized_keys
|
|
# (`from=` matters: without it that key is a root login from anywhere sshd
|
|
# accepts. `pty` is added back because the host terminal needs one.)
|
|
# 3. sshd must be listening on an address the containers can reach — a
|
|
# ListenAddress pinned to 127.0.0.1 refuses this channel and nothing else.
|
|
# 4. allow container→host on the SSH port in the host's firewall: this
|
|
# address is host-local, so it traverses filter/INPUT where a default-deny
|
|
# ufw lives — published container ports are DNAT'd and skip it, which is
|
|
# why the rest of the stack looks healthy while this one hangs.
|
|
# 5. set the vars below, then recreate the api — `env_file:` is read when a
|
|
# container is CREATED, so a restart alone changes nothing:
|
|
# docker compose --env-file .env -f docker/docker-compose.yml \
|
|
# up -d --force-recreate --no-deps api
|
|
#
|
|
# Full walkthrough, including the repair for an install that reports success and
|
|
# then fails its first host operation:
|
|
# https://openship.io/docs/troubleshooting/host-channel
|
|
# OPENSHIP_HOST_SSH_HOST=host.docker.internal
|
|
# OPENSHIP_HOST_SSH_USER=root
|
|
# OPENSHIP_HOST_SSH_PORT=22
|
|
# What `host.docker.internal` resolves to INSIDE the container. Unset means the
|
|
# daemon's own `host-gateway`, which is right on a stock rootful install and wrong
|
|
# under rootless Docker: there it lands in RootlessKit's namespace rather than the
|
|
# host's, so the host's sshd is not at that address (#482). Point it at the box's own
|
|
# LAN/bridge address in that case — or set OPENSHIP_HOST_SSH_HOST to that address
|
|
# directly. `openship up` carries whichever you set across re-runs.
|
|
# OPENSHIP_HOST_GATEWAY=10.0.0.108
|
|
# In-container path of the key. Its SOURCE on the host is OPENSHIP_HOST_KEY_PATH
|
|
# below, which docker/docker-compose.yml mounts here — so no compose file needs
|
|
# editing. Unset OPENSHIP_HOST_KEY_PATH mounts /dev/null instead, which is what
|
|
# lets the stack start on a box with no key at all.
|
|
# OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key
|
|
# ABSOLUTE path, always: a relative one resolves against docker/, not the
|
|
# directory you run `docker compose` from.
|
|
# OPENSHIP_HOST_KEY_PATH=/var/lib/openship/host-ssh/id_ed25519
|
|
# Set to false to switch host control off deliberately: no key is used, host ops
|
|
# refuse, and this box stops being offered as a deploy target. The docker socket
|
|
# is still mounted (deploys need it), so this is defense in depth, not isolation.
|
|
# OPENSHIP_HOST_CONTROL=true
|
|
|
|
# ─── OAuth login (optional) ───────────────────────────────
|
|
# GITHUB_CLIENT_ID=
|
|
# GITHUB_CLIENT_SECRET=
|
|
# GOOGLE_CLIENT_ID=
|
|
# GOOGLE_CLIENT_SECRET=
|
|
|
|
# ─── Self-hosted GitHub App (optional, no Openship Cloud/PAT required) ───────
|
|
# Configure ALL fields together. In GITHUB_AUTH_MODE=auto a complete local App
|
|
# takes priority over the cloud App and gh/PAT fallbacks. Keep the PEM and
|
|
# webhook secret only in this server-side env file; they are never stored in DB.
|
|
# See: https://openship.io/docs/guides/self-hosted-github-app
|
|
# GITHUB_AUTH_MODE=auto
|
|
# GITHUB_APP_ID=
|
|
# GITHUB_APP_SLUG=your-openship-app-slug
|
|
# GITHUB_PRIVATE_KEY_BASE64= # base64 of the App's .pem private key
|
|
# GITHUB_WEBHOOK_SECRET=
|
|
|
|
# ══════════════════════════════════════════════════════════
|
|
# Openship SaaS (CLOUD_MODE=true)
|
|
# ══════════════════════════════════════════════════════════
|
|
# In CLOUD_MODE the API always uses the canonical GitHub App (every non-App
|
|
# auth mode is rejected at boot). Register the Openship 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 owns Cloud checkout, subscriptions, credits, and metering.
|
|
# Set these in the deployed API environment. Both Compose stacks load this
|
|
# root .env; they do NOT load apps/api/.env.saas (used by dev:saas).
|
|
# Use the production account's credentials for the production SaaS.
|
|
# OBLIEN_API_URL=https://api.oblien.com
|
|
# OBLIEN_CLIENT_ID=
|
|
# OBLIEN_CLIENT_SECRET=
|
|
# OBLIEN_WEBHOOK_SECRET= # random signing secret; API registers it at startup
|
|
# Optional if the runtime API origin already resolves to the deployed API:
|
|
# OBLIEN_WEBHOOK_URL=https://api.openship.io/api/billing/oblien-webhook
|
|
|
|
# Billing feature switches (SaaS-owned; self-hosted + local proxy to the cloud).
|
|
# Both OFF by default. Set BILLING_ENABLED=true to accept subscriptions and
|
|
# BILLING_TOPUPS_ENABLED=true to also accept credit purchases, then recreate
|
|
# the API container. These are global switches, not per-organization settings.
|
|
# Billing state, usage, cancellation, and portal access remain available when
|
|
# purchases are off. A failed billing-state read is a separate setup/API issue.
|
|
BILLING_ENABLED=false
|
|
BILLING_TOPUPS_ENABLED=false
|
|
# Optional product analytics for the hosted production Cloud API only.
|
|
# Never initialized in desktop, self-hosted, development, or test modes.
|
|
# Uses the PostHog PROJECT token (phc_...), not a personal API key.
|
|
POSTHOG_ENABLED=false
|
|
# POSTHOG_PROJECT_KEY=phc_...
|
|
POSTHOG_HOST=https://us.i.posthog.com
|
|
# For an EU PostHog project: https://eu.i.posthog.com
|
|
# Exclude internal/demo accounts before enabling. Organization exclusions also
|
|
# cover background deployments, subscription snapshots and billing webhooks.
|
|
# POSTHOG_EXCLUDED_ORGANIZATION_IDS=org_internal,org_demo
|
|
# POSTHOG_EXCLUDED_USER_IDS=user_internal
|
|
# See docs/cloud-product-analytics.md for the event catalog and launch reports.
|
|
# Prices and payment configuration come from Oblien. Openship does not need
|
|
# Stripe keys, price IDs, or coupons for this billing flow.
|
|
# See docs/openship-cloud-launch.md for setup, activation, and diagnostics.
|
|
|
|
# Transactional email (optional)
|
|
# SMTP_HOST=
|
|
# SMTP_PORT=587
|
|
# SMTP_USER=
|
|
# SMTP_PASS=
|
|
# SMTP_FROM=Openship <noreply@openship.io>
|
|
|
|
# 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
|