Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,43 @@ BACKUP_GPG_PASSPHRASE=<generate-via-openssl-rand-base64-48-or-leave-blank>
# Optional healthcheck.io ping URL hit on successful backup. Unset →
# script does not ping.
HEALTHCHECK_URL=<healthcheck-io-ping-url-or-leave-blank>

# =====================================================================
# Komi Store paid-tier backend (paid-app service in docker-compose.prod.yml)
# =====================================================================
# At minimum DB_PASSWORD + KOMI_INTERNAL_TOKEN + STRIPE_SECRET_KEY +
# STRIPE_WEBHOOK_SECRET + a key-source path are required for paid-app to boot.

# Postgres credentials for komistore_paid (separate DB on the same container).
KOMI_PAID_DB_PASSWORD=<generate-via-openssl-rand-hex-32>

# Internal-auth shared secret with the future website backend.
KOMI_INTERNAL_TOKEN=<generate-via-openssl-rand-hex-32>
# Comma-separated Host header allowlist for the internal-auth path.
INTERNAL_AUTH_ALLOWED_HOSTS=api.komistore.app

# Signing-key source: "cloudflare_kv" (recommended for production) or "env".
# Compose default is "env" so a missing value fails fast on a clearly-named
# env var rather than booting cleanly and silently 404'ing on the first CF KV
# fetch. Production VPS should explicitly set this to "cloudflare_kv".
KOMI_KEY_SOURCE=cloudflare_kv
CF_ACCOUNT_ID=<cloudflare-account-id>
CF_KV_NAMESPACE_ID=<komi-signing-keys-namespace-id>
CF_API_TOKEN=<cloudflare-api-token-with-kv-read-on-this-namespace-only>
# env mode fallback only.
ED25519_PRIVATE_KEY=<pem-or-leave-blank-if-using-cf-kv>
ED25519_PUBLIC_KEY=<pem-or-leave-blank-if-using-cf-kv>

# Stripe.
STRIPE_SECRET_KEY=<stripe-secret-key>
STRIPE_WEBHOOK_SECRET=<stripe-webhook-signing-secret>
STRIPE_PRICE_ID_LIBRARY_SYNC_MONTHLY=<price_xxx>
STRIPE_PRICE_ID_LIBRARY_SYNC_YEARLY=<price_xxx>
STRIPE_PRICE_ID_DEV_PRO_MONTHLY=<price_xxx>
STRIPE_PRICE_ID_DEV_PRO_YEARLY=<price_xxx>

# Cloudflare R2 for encrypted sync bundles.
KOMI_STORAGE_BUCKET=komi-sync
KOMI_STORAGE_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
KOMI_STORAGE_ACCESS_KEY_ID=<r2-access-key-id>
KOMI_STORAGE_SECRET_ACCESS_KEY=<r2-secret-access-key>
69 changes: 69 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,75 @@ services:
max-size: "50m"
max-file: "5"

# Komi Store paid-tier backend. Lives at /opt/komi-paid-backend on the VPS,
# synced there by paid-backend/deploy.sh. Shares postgres + caddy from this
# stack so the two backends sit on one VPS without duplicating infra.
# Separate database (komistore_paid) created manually before first deploy.
#
# mem_limit set to 1.5g: VPS is 8 GB, current ceilings are postgres 3g +
# meilisearch 2g + app 2g + caddy 256m + vector 256m = 7.5g committed.
# paid-app at 1.5g brings the sum to 9g (1g overcommit). Mem limits are
# caps not reservations so the kernel handles it unless every container
# peaks simultaneously — operator should reduce free-tier app to 1.5g if
# OOM-kill events start showing up.
paid-app:
build: ../komi-paid-backend
restart: unless-stopped
Comment on lines +156 to +158
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The free-tier deploy.sh (line 26) runs docker compose -f docker-compose.prod.yml up -d --build with no service targets, so it will attempt to build paid-app from ../komi-paid-backend on every free-tier deploy. On any VPS where the paid-backend hasn't been synced (fresh rollout, separate free-tier host, etc.), the build step will fail with a "path not found" error and set -euo pipefail will abort the entire deploy — taking down the free-tier backend mid-deploy. Even on the shared VPS, paid-app gets rebuilt and restarted on every free-tier release, causing unnecessary disruption for paid users. Adding a profiles key restricts paid-app to opt-in runs (docker compose --profile paid up -d --build) and leaves the free-tier deploy unaffected.

Suggested change
paid-app:
build: ../komi-paid-backend
restart: unless-stopped
paid-app:
build: ../komi-paid-backend
restart: unless-stopped
profiles: [paid]

Fix in Claude Code

mem_limit: 1.5g
read_only: true
tmpfs:
- /tmp
environment:
# Database — shared postgres container, isolated paid-tier database.
DATABASE_URL: jdbc:postgresql://postgres:5432/komistore_paid
DATABASE_USER: komistore_paid
DATABASE_PASSWORD: ${KOMI_PAID_DB_PASSWORD}
# Internal-auth shared secret with the website backend (when it exists).
KOMI_INTERNAL_TOKEN: ${KOMI_INTERNAL_TOKEN}
INTERNAL_AUTH_ALLOWED_HOSTS: ${INTERNAL_AUTH_ALLOWED_HOSTS}
# Ed25519 signing keys. Default is `env` so a missing KOMI_KEY_SOURCE
# surfaces immediately as "env var ED25519_PRIVATE_KEY not set" rather
# than booting cleanly and silently failing at first KV fetch. Operators
# that want CF KV (production setup) must set KOMI_KEY_SOURCE=cloudflare_kv
# in /opt/github-store-backend/.env. The paid-backend SigningConfig
# additionally fail-fasts on empty CF_ACCOUNT_ID / CF_KV_NAMESPACE_ID
# when KEY_SOURCE=cloudflare_kv, so a half-configured CF deploy crashes
# at AppConfig.from(), not at signing time.
KEY_SOURCE: ${KOMI_KEY_SOURCE:-env}
CF_ACCOUNT_ID: ${CF_ACCOUNT_ID:-}
CF_KV_NAMESPACE_ID: ${CF_KV_NAMESPACE_ID:-}
CF_API_TOKEN: ${CF_API_TOKEN:-}
# Env-mode fallback for the signing key (only used when KEY_SOURCE=env).
ED25519_PRIVATE_KEY: ${ED25519_PRIVATE_KEY:-}
ED25519_PUBLIC_KEY: ${ED25519_PUBLIC_KEY:-}
# Stripe billing.
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
STRIPE_PRICE_ID_LIBRARY_SYNC_MONTHLY: ${STRIPE_PRICE_ID_LIBRARY_SYNC_MONTHLY:-}
STRIPE_PRICE_ID_LIBRARY_SYNC_YEARLY: ${STRIPE_PRICE_ID_LIBRARY_SYNC_YEARLY:-}
STRIPE_PRICE_ID_DEV_PRO_MONTHLY: ${STRIPE_PRICE_ID_DEV_PRO_MONTHLY:-}
STRIPE_PRICE_ID_DEV_PRO_YEARLY: ${STRIPE_PRICE_ID_DEV_PRO_YEARLY:-}
# Cloudflare R2 for E2E-encrypted sync bundles.
STORAGE_BUCKET: ${KOMI_STORAGE_BUCKET:-komi-sync}
STORAGE_ENDPOINT: ${KOMI_STORAGE_ENDPOINT}
STORAGE_ACCESS_KEY_ID: ${KOMI_STORAGE_ACCESS_KEY_ID}
STORAGE_SECRET_ACCESS_KEY: ${KOMI_STORAGE_SECRET_ACCESS_KEY}
APP_ENV: production
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/health"]
Comment thread
greptile-apps[bot] marked this conversation as resolved.
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"

caddy:
image: caddy:2-alpine
restart: unless-stopped
Expand Down
Loading