Files
2026-09-28 00:09:54 +03:00

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