Skip to content

Deployment

Production deploys via hostssh push-to-deploy on the self-hosted fleet. GitHub (origin) is code hosting only — pushing there does not deploy.

git push hostssh main

What a push does

  1. Checkout — the bare repo's post-receive hook checks out to
  2. /srv/hostssh/casesolo/checkout and runs hostssh-app deploy casesolo (Dockerfile build, port 3000, domains casesolo.com/www/app).

  3. Build + swap — image is built, the casesolo container is replaced.
  4. The pipeline itself runs no migrations.

  5. Migrate at container start — npm start
  6. (scripts/start-standalone.mjs) runs prisma migrate deploy against DATABASE_URL before binding, and refuses to start on failure — new code never serves an old schema. Opt out only with SKIP_MIGRATE_ON_START=true if a pipeline step owns migrations.

Manual fallback on the app box: docker exec casesolo node node_modules/prisma/build/index.js migrate deploy.

Build gotchas (all have broken deploys before)

| Symptom | Cause | Fix | |---|---|---| | Exit 255 / SIGABRT mid-build | Heap OOM (~8 GB peak) | Keep --max-old-space-size=12288 | | Module not found: tesseract.js / pdf-parse + RangeError | Built with webpack | Turbopack only — never --webpack | | Every route 502 after deploy | Standalone bound to container hostname (IPv6) | start-standalone.mjs forces HOSTNAME=0.0.0.0 | | Homepage etag unchanged after push | Build failed, old version still serving | Read the deploy hook build log |

Build identity: .dockerignore excludes .git, so the SHA resolves from NEXT_PUBLIC_BUILD_SHA / SOURCE_COMMIT build-args, then the .source-commit file, then local git rev-parse (dev only). Post-deploy: npm run verify:prod-public (optionally EXPECTED_BUILD_SHA=<sha>).

Production env (app-box deploy environment)

Minimum: DATABASE_URL, DIRECT_URL, JWT_SECRET (≥ 32 chars), USERMAILS_API_KEY, MAIL_FROM_EMAIL, NEXT_PUBLIC_APP_VERSION, NEXT_PUBLIC_BUILD_SHA, NEXT_PUBLIC_BUILD_TIME. Plus REDIS_URL, storage (R2_* or S3_*), Stripe/email/SMS/Sentry/AI keys as used. Startup fails closed without the usermails pair; pause sending with the disable_email_sms kill switch instead of removing credentials.

Scheduled dispatch (required for timed features to fire)

Scheduled emails, broadcasts, recurring tasks, and lead reminders need the cron dispatcher (src/lib/jobs/scheduled-dispatch.ts → /api/cron/dispatch):

  1. Set CRON_SECRET in the deploy environment (else the endpoint 503s).
  2. One trigger only — crontab every 15 min or a BullMQ repeatable job
  3. (needs CASESOLO_ENABLE_BULLMQ_WORKERS=true), not both.

Rollback

migrate deploy is forward-only: ship a reversing migration, never delete from prisma/migrations/. Keep changes expand/contract-safe per database-migrations. Release procedure: release-runbook.

Source: docs/dev/deployment.md