BunshipBunship

Deployment

Copy-paste deployment guide for Bunship on Vercel and Cloudflare (TurboRepo).

Deployment Model

Bunship is a TurboRepo monorepo.
Production is deployed as one site (apps/ship) that serves:

  • Marketing pages
  • App workspace
  • Admin pages
  • Docs routes and docs search API
  • API routes under /api/v1/*

No separate docs deployment is required.

1. Required Env Vars (Copy First)

Generate a secret first:

openssl rand -base64 32

Use this as a baseline production env set:

# Core auth + app URLs
BETTER_AUTH_SECRET="replace-with-long-random-secret"
AUTH_SECRET="replace-with-long-random-secret"
SITE_URL="https://YOUR_DOMAIN"
TRUSTED_ORIGINS="https://YOUR_DOMAIN"
ADMIN_EMAIL_LIST="admin@YOUR_DOMAIN"
EMAIL_FROM="Bunship <noreply@YOUR_DOMAIN>"

# OAuth
OAUTH_GITHUB_CLIENT_ID="xxx"
OAUTH_GITHUB_CLIENT_SECRET="xxx"
OAUTH_GOOGLE_CLIENT_ID="xxx"
OAUTH_GOOGLE_CLIENT_SECRET="xxx"
PUBLIC_OAUTH_GOOGLE_CLIENT_ID="xxx"

# Database + mail
DATABASE_URL="postgresql://..."
RESEND_API_KEY="re_xxx"

# Payment (Stripe / Creem)
STRIPE_SECRET_KEY="sk_live_xxx"
STRIPE_WEBHOOK_SECRET="whsec_xxx"
CREEM_API_KEY="creem_xxx"
CREEM_WEBHOOK_SECRET="creem_whsec_xxx"
PAYMENT_PROVIDER_DEFAULT="stripe"

# Storage (S3-compatible / R2)
S3_ENDPOINT="https://<account>.r2.cloudflarestorage.com"
S3_REGION="auto"
S3_ACCESS_KEY="xxx"
S3_SECRET_KEY="xxx"
S3_BUCKET="your-bucket"
PUBLIC_S3_URL_BASE="https://cdn.YOUR_DOMAIN"

# Misc required by current validators
S_GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxx"
CLOUDFLARE_ACCOUNT_ID="xxx"

If you use Better Auth, set both AUTH_SECRET and BETTER_AUTH_SECRET (keeping them identical is fine). If you use Clerk, provide PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, and CLERK_WEBHOOK_SECRET instead.

/api/v1 is now a fixed same-origin route prefix. You do not need a separate public API origin variable unless you intentionally expose auth/API on another domain, in which case set BETTER_AUTH_URL explicitly and add the extra origins to TRUSTED_ORIGINS.

PUBLIC_AUTH_PROVIDER is still a build-time variable because it selects the auth provider bundle. Other public values should use PUBLIC_* at runtime; legacy NEXT_PUBLIC_* names still work as fallbacks.

Setup references:

2. Vercel Deployment (TurboRepo)

Because this repo is TurboRepo-based, there are two valid setups.

Use the repository root as Vercel Root Directory.

Vercel fieldValue
Framework PresetNext.js
Root Directory. (repo root)
Install Commandbun install
Build Commandbunx turbo run build --filter=@bunship-ai/ship...
Output Directoryempty (Next.js default)

Why this mode: clearer monorepo behavior, better with Turbo dependency graph and cache.

Option B: App Root Mode

If you keep Root Directory = apps/ship, use:

Vercel fieldValue
Framework PresetNext.js
Root Directoryapps/ship
Include files outside root directory in Build StepEnabled
Install Commandbun install
Build Commandbun run build
Output Directoryempty (Next.js default)

In App Root Mode, keep “Include files outside root directory in Build Step” enabled, otherwise workspace packages may not resolve.

After deploy, configure Stripe webhook endpoint:

https://YOUR_DOMAIN/api/v1/webhook/stripe

Vercel references:

3. Cloudflare Deployment (OpenNext)

This repo already includes OpenNext Cloudflare config (apps/ship/open-next.config.ts + apps/ship/wrangler.jsonc).

Run from repo root:

bun install
cd apps/ship
bunx wrangler login
bun run deploy

bun run deploy executes:

  1. opennextjs-cloudflare build
  2. opennextjs-cloudflare deploy

Cloudflare references:

If you deploy on Cloudflare, set VERCEL_ENV=production in runtime variables to match production behavior expected by parts of this template.

4. GHCR Docker Image

This repo includes a GitHub Actions workflow for publishing a private Docker image to GitHub Container Registry:

  • Workflow: .github/workflows/publish-ghcr.yml
  • Image target: ghcr.io/<owner>/<repo>
  • Build file: Dockerfile
  • Trigger: pushes to main that touch app/package/build files, or manual workflow_dispatch

The workflow builds two auth-provider variants. The default image tags use Better Auth. Clerk gets explicit Clerk tags:

Auth providerTags
Better Authlatest on the default branch, branch tag, and <branch>-<sha>
Clerkclerk-latest on the default branch and clerk-<sha>

The GHCR Docker build does not require business runtime secrets by default. Image builds use build args and can read minimal build inputs from Infisical through a BuildKit secret file; business env is injected by the deployment platform when the container starts.

GitHub Actions variables (GHCR build / Infisical OIDC):

VariablePurpose
INFISICAL_IDENTITY_IDOptional OIDC identity for Infisical; see the env-config doc for the entry path
INFISICAL_PROJECT_SLUGOptional Infisical project slug
INFISICAL_ENV_SLUGOptional Infisical environment slug
INFISICAL_DOMAINOptional, defaults to https://app.infisical.com
INFISICAL_SECRET_PATHOptional Infisical secret folder path; defaults to /. If the folder is env under the root, set /env
INFISICAL_RECURSIVEOptional, defaults to false; set true only when Docker build inputs live in nested folders
INFISICAL_INCLUDE_IMPORTSOptional, defaults to true; keep the default when Infisical imports should be included
VERCEL_ENVOptional, defaults to production

The GHCR workflow keeps Load Infisical secrets, writes the output to .infisical.docker.env, and passes it to the Dockerfile as the infisical_env BuildKit secret. That file is read only during the build step and is not copied into image layers. Infisical is only used here to source the minimal Docker build inputs; it does not make business runtime variables such as SITE_URL, DATABASE_URL, S3_*, OAuth, Stripe, or email part of Docker build.

Infisical OIDC checklist: publish-ghcr.yml already has permissions.id-token: write, so OIDC auth method not found for identity usually means the INFISICAL_IDENTITY_ID machine identity still uses Universal Auth. Open it from Infisical > Project > Access Control > Machine Identities, remove Universal Auth, and add OIDC Auth. Set both OIDC Discovery URL and Issuer to https://token.actions.githubusercontent.com. Because this workflow uses GitHub Environments, use repo:<owner>/<repo>:environment:Production for the production subject and repo:<owner>/<repo>:environment:staging for the dev branch.

Docker build args:

VariablePurpose
GIT_COMMIT_SHASet by the workflow for image/frontend version metadata
VERCEL_ENVDefaults to production
PUBLIC_AUTH_PROVIDERSet by the workflow matrix to build Better Auth / Clerk image variants

Do not put SITE_URL, DATABASE_URL, S3_*, auth secrets, Stripe keys, email keys, or OAuth client IDs/secrets in Docker build. They belong to container runtime env. PUBLIC_S3_URL_BASE, PUBLIC_CLERK_PUBLISHABLE_KEY, PUBLIC_OAUTH_GOOGLE_CLIENT_ID, PUBLIC_GA_ID, PUBLIC_UMAMI_DATA_ID, and PUBLIC_SERVER_URL are also runtime public config, so the same GHCR image can be reused across environments. The old NEXT_PUBLIC_* names still work as fallbacks, but do not use them for new deployments. Do not set NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_API_PREFIX, API_PREFIX, or CORS_ORIGIN.

Set PUBLIC_SERVER_URL only when the browser-facing API origin differs from SITE_URL. With VERCEL_ENV present, requests use ${PUBLIC_SERVER_URL}/api/v1; without VERCEL_ENV, requests use ${PUBLIC_SERVER_URL}/v1.

5. One-Click Deployment Checklist

Vercel

  • Import Git repo
  • Choose Option A or Option B settings above
  • Add required env vars
  • Deploy
  • Add Stripe webhook endpoint
  • Run smoke tests below

Cloudflare

  • Create/choose Workers project
  • Add required env vars/secrets
  • bun install
  • cd apps/ship && bun run deploy
  • Bind custom domain
  • Run smoke tests below

GHCR

  • Configure Infisical OIDC variables if minimal Docker build inputs come from Infisical, or keep the workflow default build args
  • Confirm the Infisical machine identity Authentication method is OIDC Auth, not the default Universal Auth
  • Configure the OIDC Subject for the GitHub Environment: repo:<owner>/<repo>:environment:Production for main, repo:<owner>/<repo>:environment:staging for dev
  • Confirm image builds do not depend on business runtime secrets
  • Confirm SITE_URL is available only at container runtime
  • Set runtime public env such as PUBLIC_S3_URL_BASE, PUBLIC_CLERK_PUBLISHABLE_KEY, and PUBLIC_OAUTH_GOOGLE_CLIENT_ID on the container
  • Set PUBLIC_SERVER_URL only at runtime if the public API origin differs from SITE_URL, and confirm that origin exposes /api/v1 with VERCEL_ENV or /v1 without VERCEL_ENV
  • Push to main or run the workflow manually
  • Pull ghcr.io/<owner>/<repo>:latest, the branch tag, or the branch-scoped sha tag for Better Auth; use clerk-latest or clerk-<sha> for Clerk
  • Inject runtime env into the container
  • Run smoke tests below

6. Post-Deploy Smoke Test

  • /:locale/docs
  • /:locale/signin
  • /:locale/subscription
  • /:locale/admin
  • /api/v1/health (if enabled)
  • /api/docs/search