-
-
Notifications
You must be signed in to change notification settings - Fork 946
Expand file tree
/
Copy path.env.example
More file actions
267 lines (248 loc) · 15.1 KB
/
Copy path.env.example
File metadata and controls
267 lines (248 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# ──────────────────────────────────────────────────────────
# 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 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
# ─── 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=
# ══════════════════════════════════════════════════════════
# 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
# One price id per PUBLISHED price in packages/core/src/pricing/pricing.json.
# Which vars are required is decided by the catalog, not by this file: each plan
# NAMES its env var in `stripePriceEnv`, and boot logs every published price with
# no id configured (loudly in CLOUD_MODE). An unset id → 503
# BILLING_NOT_CONFIGURED at checkout; a tier with no published price → 400
# BILLING_PLAN_NOT_PURCHASABLE. Monthly prices are live ($10/$39/$99); ANNUAL is
# not published yet (annual.enabled=false), so the annual vars are unused until
# it is — set them only alongside publishing annual prices in the catalog.
# STRIPE_PRICE_STARTER_MONTHLY= # Starter, $10/mo
# STRIPE_PRICE_PRO_MONTHLY= # Pro, $39/mo
# STRIPE_PRICE_TEAM_MONTHLY= # Scale, $99/mo
# STRIPE_PRICE_STARTER_ANNUAL=
# STRIPE_PRICE_PRO_ANNUAL=
# 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=
#
# Promo codes are minted with the Stripe key above, from inside the API container:
# docker compose exec api bun --cwd apps/api scripts/promo-code.ts create --percent 20 --code LAUNCH20
# Checkout already renders the redemption field (allow_promotion_codes).
#
# CAMPAIGNS (automatic, time-bounded discounts — e.g. 50% off at launch) are declared
# in packages/core/src/pricing/pricing.json `campaigns[]`. Each one NAMES the env var
# holding its Stripe coupon id; set that var here. Boot compares the catalog's
# percentage against the live coupon and logs a mismatch, so the page can't advertise
# 50% while Stripe bills 40%. NOTE a running campaign HIDES the promo-code box —
# Stripe permits an automatic discount or a redeemable code, never both.
# STRIPE_COUPON_LAUNCH50=
# 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